#!/usr/bin/env python3
"""FDC Portal Bridge — Download this, run it. That's it.
Connects your Claude Code subscription to the FDC Portal.
Uses claude --print (your already-authenticated CLI). No API keys needed.
"""

import json, os, re, subprocess, shutil, sys
import urllib.request, urllib.error
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler

OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")

PORT = 3456
PORTAL_BOTS_ROOT = Path("C:/claude/portal-bots")
# Env vars that survive into the spawned Claude Code subprocess. Everything else is stripped
# so portal bots can't read host secrets (vault paths, NAS creds, MCP refs, etc.).
BOT_NAME_RE = re.compile(r"^[A-Z0-9_]{1,32}$")

# Conservative blocklist — strip env vars that look like secrets or host-scoped credentials,
# pass everything else through so claude's runtime requirements stay intact.
# Suffix patterns: ends with these → block.
ENV_BLOCK_SUFFIX = ("_API_KEY", "_KEY", "_TOKEN", "_SECRET", "_PASSWORD", "_PIN")
# Prefix patterns: starts with these → block.
ENV_BLOCK_PREFIX = (
    "VAULT_", "NAS_", "SUPABASE_", "AWS_", "GOOGLE_", "AZURE_",
    "GITHUB_", "OPENAI_", "ANTHROPIC_", "POLLINATIONS_", "GROQ_", "MISTRAL_",
)


def is_blocked_env(name: str) -> bool:
    upper = name.upper()
    if any(upper.endswith(s) for s in ENV_BLOCK_SUFFIX):
        return True
    if any(upper.startswith(p) for p in ENV_BLOCK_PREFIX):
        return True
    return False


def sanitize_bot_name(raw: str) -> str | None:
    if not raw:
        return None
    cleaned = re.sub(r"[^A-Za-z0-9_]", "", raw).upper()[:32]
    return cleaned if BOT_NAME_RE.match(cleaned) else None


def bot_home_paths(safe_name: str):
    """Return (home_dir, cwd_dir, mcp_config_path) for a sanitized bot name; create lazily.
    The mcp.json starts empty so the bot inherits NO host MCPs. Wizard / settings can wire
    per-bot MCPs into this file later."""
    bot_root = PORTAL_BOTS_ROOT / safe_name
    bot_home = bot_root / ".home"
    bot_cwd = bot_root / "workspace"
    claude_dir = bot_home / ".claude"
    claude_dir.mkdir(parents=True, exist_ok=True)
    bot_cwd.mkdir(parents=True, exist_ok=True)
    mcp_path = claude_dir / "mcp.json"
    if not mcp_path.exists():
        mcp_path.write_text('{"mcpServers": {}}', encoding="utf-8")
    return bot_home, bot_cwd, mcp_path


def has_claude():
    return shutil.which("claude") is not None


class BridgeHandler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        print(f"[bridge] {args[0]}" if args else "")

    def _cors(self, resp_code=200, content_type="application/json; charset=utf-8"):
        self.send_response(resp_code)
        self.send_header("Content-Type", content_type)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-FDC-Bot")
        self.end_headers()

    def do_OPTIONS(self):
        self._cors(204)

    def do_GET(self):
        if self.path == "/health":
            ok = has_claude()
            ollama_ok = False
            try:
                with urllib.request.urlopen(f"{OLLAMA_URL}/api/version", timeout=2) as r:
                    ollama_ok = r.status == 200
            except Exception:
                pass
            self._cors()
            self.wfile.write(json.dumps({
                "status": "ok" if ok else "no_cli",
                "port": PORT,
                "claude": ok,
                "ollama": ollama_ok,
            }).encode())
        elif self.path == "/v1/ollama/models":
            # Proxy Ollama's /api/tags so the portal can list installed models.
            try:
                with urllib.request.urlopen(f"{OLLAMA_URL}/api/tags", timeout=4) as r:
                    body = r.read()
                self._cors()
                self.wfile.write(body)
            except Exception as e:
                self._cors(502)
                self.wfile.write(json.dumps({"error": f"ollama: {e}"}).encode())
        elif self.path == "/v1/models":
            self._cors()
            self.wfile.write(json.dumps({"data": [
                {"id": "claude-opus-4-20250514", "object": "model"},
                {"id": "claude-sonnet-4-20250514", "object": "model"},
                {"id": "claude-haiku-4-5-20251001", "object": "model"},
            ]}).encode())
        else:
            self._cors(404)
            self.wfile.write(b'{"error":"not found"}')

    def do_POST(self):
        if self.path == "/v1/ollama/chat":
            return self._handle_ollama_chat()
        if self.path != "/v1/chat/completions":
            self._cors(404)
            self.wfile.write(b'{"error":"not found"}')
            return

        if not has_claude():
            self._cors(401)
            self.wfile.write(b'{"error":"claude CLI not found in PATH"}')
            return

        # Per-bot isolation: portal sends X-FDC-Bot to identify which bot is making the call.
        # Each bot gets its own USERPROFILE / cwd / CLAUDE_BOT_NAME so they can't see each other's
        # files, MCP configs, or env. Mirrors the per-bot pattern used by ALIEN25 CLI bots.
        raw_bot = self.headers.get("X-FDC-Bot", "")
        safe_name = sanitize_bot_name(raw_bot)
        if not safe_name:
            self._cors(400)
            self.wfile.write(b'{"error":"X-FDC-Bot header required (alphanumeric+underscore, 1-32 chars)"}')
            return
        bot_home, bot_cwd, mcp_path = bot_home_paths(safe_name)

        length = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(length))

        messages = body.get("messages", [])
        model = body.get("model", "claude-sonnet-4-20250514")

        system_msg = ""
        prompt_parts = []
        for m in messages:
            if m.get("role") == "system":
                system_msg = m.get("content", "")
            elif m.get("role") == "user":
                prompt_parts.append(m.get("content", ""))
            elif m.get("role") == "assistant":
                prompt_parts.append(f"[Previous response]: {m.get('content', '')}")

        prompt = "\n\n".join(prompt_parts)
        if system_msg:
            prompt = f"{system_msg}\n\n---\n\n{prompt}"

        args = [
            "claude", "--print",
            "--output-format", "json",
            "--model", model,
            "--no-session-persistence",
            # Per-bot MCP isolation: use ONLY this empty file, ignore host MCP config.
            # Default is no MCPs; wizard wires per-bot entries into mcp.json over time.
            "--mcp-config", str(mcp_path),
            "--strict-mcp-config",
        ]

        # Filter env: pass everything through EXCEPT known-secret-shaped vars (see is_blocked_env).
        # Stamp the per-bot identity so any bot-aware hooks can read it. File isolation is via cwd.
        safe_env = {k: v for k, v in os.environ.items() if not is_blocked_env(k)}
        safe_env["CLAUDE_BOT_NAME"] = safe_name
        safe_env["PYTHONIOENCODING"] = "utf-8"

        try:
            proc = subprocess.run(
                args,
                input=prompt.encode("utf-8"),
                capture_output=True,
                timeout=120,
                cwd=str(bot_cwd),
                env=safe_env,
            )
            if proc.returncode != 0:
                self._cors(502)
                err = proc.stderr.decode("utf-8", errors="replace")[:500]
                self.wfile.write(json.dumps({"error": err}).encode())
                return

            raw = proc.stdout.decode("utf-8", errors="replace")
            raw_stderr = proc.stderr.decode("utf-8", errors="replace")
            try:
                parsed = json.loads(raw)
                content = parsed.get("result") or parsed.get("text") or parsed.get("content") or ""
                usage_raw = parsed.get("usage", {})
                input_tokens = usage_raw.get("input_tokens", 0) + usage_raw.get("cache_read_input_tokens", 0)
                output_tokens = usage_raw.get("output_tokens", 0)
                cost = parsed.get("total_cost_usd", 0)
                if not content.strip():
                    # Log full diagnostic context when CLI returns empty
                    print(f"[bridge] EMPTY response from model={model}")
                    print(f"[bridge]   parsed keys: {list(parsed.keys())}")
                    print(f"[bridge]   raw stdout ({len(raw)}b): {raw[:500]}")
                    if raw_stderr.strip():
                        print(f"[bridge]   stderr: {raw_stderr[:500]}")
            except json.JSONDecodeError:
                content = raw.strip()
                input_tokens = 0
                output_tokens = 0
                cost = 0
                if not content:
                    print(f"[bridge] NON-JSON empty response from model={model}")
                    if raw_stderr.strip():
                        print(f"[bridge]   stderr: {raw_stderr[:500]}")

            result = {
                "choices": [{"message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
                "usage": {"prompt_tokens": input_tokens, "completion_tokens": output_tokens, "cost_usd": cost},
                "model": model,
            }
            self._cors()
            self.wfile.write(json.dumps(result).encode())

        except subprocess.TimeoutExpired:
            self._cors(504)
            self.wfile.write(b'{"error":"claude --print timed out (120s)"}')
        except Exception as e:
            self._cors(502)
            self.wfile.write(json.dumps({"error": str(e)}).encode())

    # ----- Ollama relay --------------------------------------------------
    # The portal posts the same {model, messages[]} shape it sends to Claude.
    # We POST it through to the user's local Ollama at OLLAMA_URL and reshape
    # the response into OpenAI-compatible form so the portal's frontend can
    # treat both bridges identically.
    def _handle_ollama_chat(self):
        length = int(self.headers.get("Content-Length", 0))
        try:
            body = json.loads(self.rfile.read(length))
        except Exception:
            self._cors(400)
            self.wfile.write(b'{"error":"bad json"}')
            return

        model = body.get("model") or "dolphin3:8b"
        messages = body.get("messages") or []
        options = body.get("options") or {"temperature": 0.7, "num_predict": 2048, "num_ctx": 32768}

        ollama_body = json.dumps({
            "model": model,
            "messages": messages,
            "stream": False,
            "options": options,
        }).encode("utf-8")

        try:
            req = urllib.request.Request(
                f"{OLLAMA_URL}/api/chat",
                data=ollama_body,
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=300) as r:
                raw = r.read().decode("utf-8", errors="replace")
        except urllib.error.HTTPError as e:
            self._cors(502)
            self.wfile.write(json.dumps({"error": f"ollama HTTP {e.code}: {e.reason}"}).encode())
            return
        except Exception as e:
            self._cors(502)
            self.wfile.write(json.dumps({"error": f"ollama unreachable at {OLLAMA_URL}: {e}"}).encode())
            return

        try:
            parsed = json.loads(raw)
            content = (parsed.get("message") or {}).get("content", "")
            prompt_eval = parsed.get("prompt_eval_count", 0)
            eval_count = parsed.get("eval_count", 0)
        except Exception:
            content = raw
            prompt_eval = 0
            eval_count = 0

        result = {
            "choices": [{"message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
            "usage": {"prompt_tokens": prompt_eval, "completion_tokens": eval_count, "cost_usd": 0},
            "model": model,
        }
        self._cors()
        self.wfile.write(json.dumps(result).encode())


if __name__ == "__main__":
    ok = has_claude()
    print(f"[bridge] claude CLI: {'found' if ok else 'NOT FOUND — install Claude Code first'}")
    print(f"[bridge] http://localhost:{PORT}")
    print(f"[bridge] Portal will auto-detect this bridge")
    try:
        HTTPServer(("127.0.0.1", PORT), BridgeHandler).serve_forever()
    except KeyboardInterrupt:
        print("\n[bridge] Stopped")
