#!/usr/bin/env python3
"""
AgentJoy Bridge
================

A tiny, dependency-free bridge that converts tool/agent activity into AgentJoy run events.

Supported integrations (Phase 1):
- OpenAI Codex CLI (non-interactive JSON mode)
- Claude Code hooks (command hooks / plugin hooks)
- ChatGPT Apps SDK template (separate folder)

This file is intentionally "single file" for beginner-friendly setup.
"""
from __future__ import annotations

import argparse
import json
import os
import queue
import subprocess
import sys
import threading
import time
import traceback
from dataclasses import dataclass
from datetime import datetime, timezone, timedelta
from typing import Any, Dict, Iterable, List, Optional, Tuple
from urllib import request as urlrequest
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin

DEFAULT_API_BASE = "http://127.0.0.1:8000"

# ---------------------------
# Config helpers
# ---------------------------

def _home_dir() -> str:
    return os.path.expanduser("~")


def _config_dir() -> str:
    return os.path.join(_home_dir(), ".agentjoy")


def _config_path() -> str:
    return os.path.join(_config_dir(), "config.json")


def load_config() -> Dict[str, Any]:
    path = _config_path()
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return {}
    except Exception:
        # If config is corrupted, ignore (don't crash automations)
        return {}


def save_config(cfg: Dict[str, Any]) -> None:
    os.makedirs(_config_dir(), exist_ok=True)
    path = _config_path()
    with open(path, "w", encoding="utf-8") as f:
        json.dump(cfg, f, ensure_ascii=False, indent=2)


# ---------------------------
# HTTP client (stdlib-only)
# ---------------------------

@dataclass
class ApiClient:
    api_base: str
    api_key: Optional[str] = None
    user_token: Optional[str] = None
    timeout_s: float = 10.0

    def _headers(self, use_user: bool = False) -> Dict[str, str]:
        h = {"Content-Type": "application/json"}
        if use_user and self.user_token:
            h["Authorization"] = f"Bearer {self.user_token}"
        elif self.api_key:
            h["X-AgentJoy-Key"] = self.api_key
        return h

    def _request(self, method: str, path: str, body: Optional[Dict[str, Any]] = None, *, use_user: bool = False) -> Any:
        url = urljoin(self.api_base.rstrip("/") + "/", path.lstrip("/"))
        data = None
        if body is not None:
            data = json.dumps(body, ensure_ascii=False).encode("utf-8")
        req = urlrequest.Request(url, data=data, method=method, headers=self._headers(use_user=use_user))
        try:
            with urlrequest.urlopen(req, timeout=self.timeout_s) as resp:
                raw = resp.read().decode("utf-8")
                if not raw:
                    return None
                try:
                    return json.loads(raw)
                except json.JSONDecodeError:
                    return raw
        except HTTPError as e:
            raw = e.read().decode("utf-8", errors="ignore")
            raise RuntimeError(f"HTTP {e.code} {method} {url}: {raw}") from None
        except URLError as e:
            raise RuntimeError(f"Network error {method} {url}: {e}") from None

    def get(self, path: str) -> Any:
        return self._request("GET", path)

    def post(self, path: str, body: Dict[str, Any], *, use_user: bool = False) -> Any:
        return self._request("POST", path, body=body, use_user=use_user)


# ---------------------------
# AgentJoy API wrappers
# ---------------------------

def api_health(client: ApiClient) -> bool:
    try:
        r = client.get("/health")
        return bool(r and r.get("status") == "ok")
    except Exception:
        return False


def api_create_run(
    client: ApiClient,
    title: str,
    *,
    mode: str = "default",
    pack: str = "default",
    metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    body = {
        "title": title,
        "mode": mode,
        "pack": pack,
        "metadata": metadata or {},
    }
    return client.post("/v1/runs", body)


def api_post_events(client: ApiClient, run_id: str, events: List[Dict[str, Any]]) -> None:
    # batch endpoint exists, but we keep a simple loop for compatibility
    # (and because failures are easier to attribute per-event).
    for e in events:
        client.post(f"/v1/runs/{run_id}/events", e)


def api_share_run(client: ApiClient, run_id: str, expires_in_hours: int = 72) -> Optional[str]:
    """Returns share URL if possible, else None."""
    body = {"expires_in_hours": expires_in_hours}
    # Prefer API-key based endpoint (new in this bundle).
    try:
        r = client.post(f"/v1/runs/{run_id}/share_by_key", body)
        return r.get("url") if isinstance(r, dict) else None
    except Exception:
        pass
    # Fallback to user-token endpoint if provided.
    if client.user_token:
        try:
            r = client.post(f"/v1/runs/{run_id}/share", body, use_user=True)
            return r.get("url") if isinstance(r, dict) else None
        except Exception:
            return None
    return None


# ---------------------------
# Redaction helpers
# ---------------------------

SENSITIVE_KEYS = {"api_key", "apikey", "token", "secret", "password", "authorization", "bearer"}

def redact(obj: Any, *, max_chars: int = 4000) -> Any:
    """Best-effort redaction + truncation for tool inputs/outputs.

    We don't try to be perfect. We just avoid obvious footguns and keep payload small.
    """
    try:
        if obj is None:
            return None
        if isinstance(obj, (int, float, bool)):
            return obj
        if isinstance(obj, str):
            s = obj
            if len(s) > max_chars:
                s = s[:max_chars] + "…(truncated)"
            return s
        if isinstance(obj, list):
            return [redact(x, max_chars=max_chars) for x in obj[:200]]
        if isinstance(obj, dict):
            out = {}
            for k, v in list(obj.items())[:200]:
                lk = str(k).lower()
                if lk in SENSITIVE_KEYS:
                    out[k] = "***redacted***"
                else:
                    out[k] = redact(v, max_chars=max_chars)
            return out
        # Fallback: stringification
        return redact(str(obj), max_chars=max_chars)
    except Exception:
        return "***redacted***"


# ---------------------------
# Codex bridge
# ---------------------------

class CodexBridge:
    def __init__(self, client: ApiClient, *, title: str, pack: str = "default", mode: str = "default", share: bool = True):
        self.client = client
        self.title = title
        self.pack = pack
        self.mode = mode
        self.share = share
        self.run: Optional[Dict[str, Any]] = None
        self.run_id: Optional[str] = None
        self.share_url: Optional[str] = None
        self._final_messages: List[str] = []
        self._q: "queue.Queue[Dict[str, Any]]" = queue.Queue()
        self._stop = threading.Event()
        self._sender = threading.Thread(target=self._sender_loop, daemon=True)

    def start(self) -> None:
        self.run = api_create_run(self.client, self.title, mode=self.mode, pack=self.pack, metadata={"source": "codex"})
        self.run_id = self.run["run"]["id"]
        if self.share:
            self.share_url = api_share_run(self.client, self.run_id)  # may be None
        self._sender.start()
        # initial note
        self.enqueue_event({
            "type": "system.comment",
            "phase": "start",
            "message": "Codex の実行を監視しています。",
            "detail": None,
            "tool": None,
        })

    def enqueue_event(self, e: Dict[str, Any]) -> None:
        if not self.run_id:
            return
        self._q.put(e)

    def _sender_loop(self) -> None:
        buf: List[Dict[str, Any]] = []
        last_flush = time.time()
        while not self._stop.is_set() or not self._q.empty():
            try:
                e = self._q.get(timeout=0.2)
                buf.append(e)
            except queue.Empty:
                pass
            now = time.time()
            if buf and (len(buf) >= 5 or (now - last_flush) >= 0.75):
                try:
                    api_post_events(self.client, self.run_id, buf)
                except Exception:
                    # Avoid killing the bridge. Print once and keep going.
                    print("[agentjoy] failed to post events (continuing)", file=sys.stderr)
                buf = []
                last_flush = now
        # final flush
        if buf and self.run_id:
            try:
                api_post_events(self.client, self.run_id, buf)
            except Exception:
                pass

    def stop(self, ok: bool = True, *, message: Optional[str] = None) -> None:
        if ok:
            self.enqueue_event({
                "type": "run.completed",
                "phase": "done",
                "message": message or "Codex の処理が完了しました。",
                "detail": None,
                "tool": None,
                "artifacts": None,
            })
        else:
            self.enqueue_event({
                "type": "run.failed",
                "phase": "done",
                "message": message or "Codex の処理が失敗しました。",
                "detail": None,
                "tool": None,
            })
        # let sender flush
        self._stop.set()
        self._sender.join(timeout=3.0)

    def on_jsonl_event(self, line_obj: Dict[str, Any]) -> None:
        etype = (line_obj.get("type") or "").strip()
        item = line_obj.get("item") or {}
        ts = line_obj.get("timestamp") or line_obj.get("ts")

        # Thread / turn lifecycle
        if etype == "thread.started":
            self.enqueue_event({"type": "system.comment", "phase": "start", "message": "Codex: thread started", "detail": None})
            return
        if etype in {"thread.completed", "thread.ended"}:
            self.enqueue_event({"type": "system.comment", "phase": "done", "message": "Codex: thread completed", "detail": None})
            return

        # Item lifecycle
        if etype in {"item.started", "item.delta", "item.completed"} and isinstance(item, dict):
            itype = (item.get("type") or "").strip()
            status = (item.get("status") or "").strip()
            item_id = item.get("id")

            # Most useful: agent_message + command execution-ish.
            if itype in {"agent_message", "assistant_message"}:
                txt = item.get("text") or item.get("content") or ""
                txt = str(txt)
                if txt:
                    # Store for printing final answer.
                    if etype == "item.completed":
                        self._final_messages.append(txt)
                    # Stream partial as comments (lightly throttled by sender).
                    self.enqueue_event({
                        "type": "system.comment",
                        "phase": "think" if status == "in_progress" else "report",
                        "message": txt.splitlines()[0][:120] if txt else "assistant",
                        "detail": txt if len(txt) <= 2000 else (txt[:2000] + "…"),
                    })
                return

            # Heuristic: command execution
            cmd = item.get("command") or item.get("cmd") or (item.get("input") or {}).get("command") if isinstance(item.get("input"), dict) else None
            if cmd:
                cmd_s = str(cmd)
                if etype == "item.started":
                    self.enqueue_event({
                        "type": "tool.called",
                        "phase": "execute",
                        "message": f"実行: {cmd_s}",
                        "detail": None,
                        "tool": {"name": "bash", "input": {"command": cmd_s}},
                    })
                elif etype == "item.completed":
                    out = item.get("output") or item.get("result") or item.get("stdout") or ""
                    code = item.get("exit_code") or item.get("code")
                    detail = ""
                    if code is not None:
                        detail += f"exit_code={code}\n"
                    if out:
                        detail += str(out)
                    self.enqueue_event({
                        "type": "tool.completed" if str(code) in {"0", "None"} else "tool.completed",
                        "phase": "execute",
                        "message": f"完了: {cmd_s}",
                        "detail": detail[:4000] + ("…(truncated)" if len(detail) > 4000 else ""),
                        "tool": {"name": "bash", "input": {"command": cmd_s}, "output": redact(out)},
                    })
                return

            # Generic tool-ish item: show type
            if itype and etype == "item.started":
                self.enqueue_event({
                    "type": "tool.called",
                    "phase": "work",
                    "message": f"{itype} を開始",
                    "detail": None,
                    "tool": {"name": itype, "input": redact(item.get("input") or {})},
                })
                return
            if itype and etype == "item.completed":
                self.enqueue_event({
                    "type": "tool.completed",
                    "phase": "work",
                    "message": f"{itype} が完了",
                    "detail": None,
                    "tool": {"name": itype, "output": redact(item)},
                })
                return

        # Anything else: keep quiet.

    def final_answer(self) -> str:
        # Prefer last completed message.
        if self._final_messages:
            return self._final_messages[-1].strip()
        return ""


def run_codex_subprocess(prompt: str, extra_args: List[str]) -> subprocess.Popen:
    # Use Codex CLI in non-interactive JSON mode.
    # Docs: `codex exec --json "..."` streams JSONL events to stdout.
    cmd = ["codex", "exec", "--json", prompt] + extra_args
    return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)


def codex_run_flow(args: argparse.Namespace, client: ApiClient) -> int:
    title = args.title or f"Codex: {args.prompt[:60]}"
    bridge = CodexBridge(client, title=title, pack=args.pack, mode=args.mode, share=args.share)
    bridge.start()

    # Print URLs to stderr so stdout can be reserved for final answer.
    print(f"[agentjoy] run_id={bridge.run_id}", file=sys.stderr)
    if bridge.share_url:
        print(f"[agentjoy] share_url={bridge.share_url}", file=sys.stderr)
    else:
        print("[agentjoy] share_url=（未作成。必要ならConsoleでShare作成してください）", file=sys.stderr)
    if bridge.run and bridge.run.get("run") and bridge.run["run"].get("console_url"):
        print(f"[agentjoy] console_url={bridge.run['run']['console_url']}", file=sys.stderr)

    proc = run_codex_subprocess(args.prompt, args.codex_args or [])
    assert proc.stdout is not None
    assert proc.stderr is not None

    # Forward stderr in background (so codex progress/errors aren't lost)
    def _stderr_forward():
        for line in proc.stderr:
            sys.stderr.write(line)
    t = threading.Thread(target=_stderr_forward, daemon=True)
    t.start()

    ok = True
    try:
        for line in proc.stdout:
            line = line.strip()
            if not line:
                continue
            try:
                obj = json.loads(line)
            except json.JSONDecodeError:
                continue
            bridge.on_jsonl_event(obj)
    except KeyboardInterrupt:
        ok = False
        bridge.stop(ok=False, message="中断されました。")
        proc.kill()
        return 130
    except Exception as e:
        ok = False
        bridge.stop(ok=False, message=f"ブリッジ側エラー: {e}")
        proc.kill()
        return 1

    rc = proc.wait()
    ok = ok and (rc == 0)
    bridge.stop(ok=ok, message="Codex が終了しました。")
    ans = bridge.final_answer()
    if ans:
        # Print final answer to stdout for convenience.
        print(ans)
    return 0 if ok else rc or 1


def codex_stream_flow(args: argparse.Namespace, client: ApiClient) -> int:
    title = args.title or "Codex (stream)"
    bridge = CodexBridge(client, title=title, pack=args.pack, mode=args.mode, share=args.share)
    bridge.start()
    print(f"[agentjoy] run_id={bridge.run_id}", file=sys.stderr)
    if bridge.share_url:
        print(f"[agentjoy] share_url={bridge.share_url}", file=sys.stderr)

    ok = True
    try:
        for line in sys.stdin:
            line = line.strip()
            if not line:
                continue
            try:
                obj = json.loads(line)
            except json.JSONDecodeError:
                continue
            bridge.on_jsonl_event(obj)
    except KeyboardInterrupt:
        ok = False
    except Exception as e:
        ok = False
        print(f"[agentjoy] error: {e}", file=sys.stderr)
    bridge.stop(ok=ok, message="Codex stream ended.")
    ans = bridge.final_answer()
    if ans:
        print(ans)
    return 0 if ok else 1


# ---------------------------
# Claude hook helper (standalone)
# ---------------------------

SESSION_MAP_PATH = os.path.join(_config_dir(), "claude_sessions.json")

def _load_session_map() -> Dict[str, str]:
    try:
        with open(SESSION_MAP_PATH, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return {}
    except Exception:
        return {}

def _save_session_map(m: Dict[str, str]) -> None:
    os.makedirs(_config_dir(), exist_ok=True)
    tmp = SESSION_MAP_PATH + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(m, f, ensure_ascii=False, indent=2)
    os.replace(tmp, SESSION_MAP_PATH)

def claude_hook_flow(args: argparse.Namespace, client: ApiClient) -> int:
    try:
        inp = json.load(sys.stdin)
    except Exception:
        return 0  # Claude hooks must be resilient

    hook = inp.get("hook_event_name") or ""
    session_id = inp.get("session_id") or "unknown"
    tool_name = inp.get("tool_name")
    tool_input = inp.get("tool_input")
    tool_response = inp.get("tool_response")
    message = inp.get("message")
    reason = inp.get("reason")

    sessions = _load_session_map()
    run_id = sessions.get(session_id)

    def _ensure_run() -> str:
        nonlocal run_id
        if run_id:
            return run_id
        title = args.title or f"Claude Code: {session_id}"
        run = api_create_run(client, title, mode=args.mode, pack=args.pack, metadata={"source": "claude_code", "session_id": session_id})
        run_id = run["run"]["id"]
        sessions[session_id] = run_id
        _save_session_map(sessions)
        # Share link if possible
        if args.share:
            share_url = api_share_run(client, run_id)
            if share_url:
                # Show to user as systemMessage (Claude Code hook JSON output).
                out = {"systemMessage": f"AgentJoy tracking started: {share_url}", "suppressOutput": True}
                print(json.dumps(out, ensure_ascii=False))
        return run_id

    # SessionStart: create a run and announce
    if hook == "SessionStart":
        _ensure_run()
        api_post_events(client, run_id, [{
            "type": "system.comment",
            "phase": "start",
            "message": "Claude Code セッション開始",
            "detail": inp.get("cwd") or None,
        }])
        return 0

    # Other hooks: ensure run exists but do not spam systemMessage.
    _ensure_run()

    if hook == "PreToolUse" and tool_name:
        api_post_events(client, run_id, [{
            "type": "tool.called",
            "phase": "execute",
            "message": f"Tool: {tool_name}",
            "detail": None,
            "tool": {"name": tool_name, "input": redact(tool_input)},
        }])
        return 0

    if hook == "PostToolUse" and tool_name:
        # Determine success if tool_response has a "success" bool
        success = True
        if isinstance(tool_response, dict) and "success" in tool_response:
            success = bool(tool_response.get("success"))
        etype = "tool.completed" if success else "tool.failed"
        api_post_events(client, run_id, [{
            "type": etype,
            "phase": "execute",
            "message": f"Tool done: {tool_name}",
            "detail": None,
            "tool": {"name": tool_name, "input": redact(tool_input), "output": redact(tool_response)},
        }])
        return 0

    if hook == "Notification" and message:
        api_post_events(client, run_id, [{
            "type": "warning" if inp.get("notification_type") else "system.comment",
            "phase": "notify",
            "message": str(message)[:200],
            "detail": str(message),
        }])
        return 0

    if hook in {"Stop", "SubagentStop"}:
        api_post_events(client, run_id, [{
            "type": "system.comment",
            "phase": "stop",
            "message": "停止要求",
            "detail": json.dumps({"stop_reason": inp.get("stop_reason") or inp.get("reason")}, ensure_ascii=False),
        }])
        return 0

    if hook == "SessionEnd":
        api_post_events(client, run_id, [{
            "type": "run.completed",
            "phase": "done",
            "message": f"Claude Code セッション終了 ({reason or 'unknown'})",
            "detail": None,
        }])
        # Cleanup mapping
        try:
            sessions.pop(session_id, None)
            _save_session_map(sessions)
        except Exception:
            pass
        return 0

    return 0


# ---------------------------
# CLI
# ---------------------------

def build_client_from_args(args: argparse.Namespace) -> ApiClient:
    cfg = load_config()
    api_base = getattr(args, "api_base", None) or os.environ.get("AGENTJOY_API_BASE") or cfg.get("api_base") or DEFAULT_API_BASE
    api_key = getattr(args, "api_key", None) or os.environ.get("AGENTJOY_API_KEY") or cfg.get("api_key")
    user_token = getattr(args, "user_token", None) or os.environ.get("AGENTJOY_USER_TOKEN") or cfg.get("user_token")
    return ApiClient(api_base=api_base, api_key=api_key, user_token=user_token, timeout_s=10.0)


def main(argv: Optional[List[str]] = None) -> int:
    p = argparse.ArgumentParser(prog="agentjoy", description="Bridge Codex/Claude activity into AgentJoy runs.")
    sub = p.add_subparsers(dest="cmd", required=True)

    # configure
    pcfg = sub.add_parser("configure", help="Save AgentJoy API settings to ~/.agentjoy/config.json")
    pcfg.add_argument("--api-base", default=None)
    pcfg.add_argument("--api-key", default=None)
    pcfg.add_argument("--user-token", default=None)
    pcfg.add_argument("--pack", default=None)
    pcfg.add_argument("--mode", default=None)

    # health
    phealth = sub.add_parser("health", help="Check AgentJoy backend health")
    phealth.add_argument("--api-base", default=None)

    # codex-run
    pcodex = sub.add_parser("codex-run", help="Run `codex exec --json` and stream into AgentJoy")
    pcodex.add_argument("prompt", help="Prompt passed to codex exec")
    pcodex.add_argument("--title", default=None)
    pcodex.add_argument("--pack", default="default")
    pcodex.add_argument("--mode", default="default")
    pcodex.add_argument("--share", action="store_true", default=True)
    pcodex.add_argument("--no-share", dest="share", action="store_false")
    pcodex.add_argument("--api-base", default=None)
    pcodex.add_argument("--api-key", default=None)
    pcodex.add_argument("--user-token", default=None)
    pcodex.add_argument("--codex-args", nargs=argparse.REMAINDER, help="Extra args after -- passed to codex")

    # codex-stream
    pstream = sub.add_parser("codex-stream", help="Read Codex JSONL from stdin and stream into AgentJoy")
    pstream.add_argument("--title", default="Codex (stdin)")
    pstream.add_argument("--pack", default="default")
    pstream.add_argument("--mode", default="default")
    pstream.add_argument("--share", action="store_true", default=True)
    pstream.add_argument("--no-share", dest="share", action="store_false")
    pstream.add_argument("--api-base", default=None)
    pstream.add_argument("--api-key", default=None)
    pstream.add_argument("--user-token", default=None)

    # claude-hook
    pcl = sub.add_parser("claude-hook", help="Claude Code hook entrypoint (reads JSON from stdin).")
    pcl.add_argument("--title", default=None)
    pcl.add_argument("--pack", default="default")
    pcl.add_argument("--mode", default="default")
    pcl.add_argument("--share", action="store_true", default=True)
    pcl.add_argument("--no-share", dest="share", action="store_false")
    pcl.add_argument("--api-base", default=None)
    pcl.add_argument("--api-key", default=None)
    pcl.add_argument("--user-token", default=None)

    args = p.parse_args(argv)

    if args.cmd == "configure":
        cfg = load_config()
        if args.api_base:
            cfg["api_base"] = args.api_base
        if args.api_key:
            cfg["api_key"] = args.api_key
        if args.user_token:
            cfg["user_token"] = args.user_token
        if args.pack:
            cfg["default_pack"] = args.pack
        if args.mode:
            cfg["default_mode"] = args.mode
        save_config(cfg)
        print(f"Saved: {_config_path()}")
        return 0

    if args.cmd == "health":
        client = build_client_from_args(args)
        ok = api_health(client)
        print("ok" if ok else "failed")
        return 0 if ok else 1

    client = build_client_from_args(args)
    if not client.api_key and args.cmd in {"codex-run", "codex-stream", "claude-hook"}:
        print("[agentjoy] ERROR: api_key is required. Set AGENTJOY_API_KEY or run `agentjoy configure --api-key ...`", file=sys.stderr)
        return 2

    if args.cmd == "codex-run":
        # Ensure codex exists
        if shutil_which("codex") is None:
            print("[agentjoy] ERROR: `codex` command not found. Install OpenAI Codex CLI first.", file=sys.stderr)
            return 2
        extra = args.codex_args or []
        # `--` separator is typical; if present, drop it
        if extra and extra[0] == "--":
            extra = extra[1:]
        args.codex_args = extra
        return codex_run_flow(args, client)

    if args.cmd == "codex-stream":
        return codex_stream_flow(args, client)

    if args.cmd == "claude-hook":
        return claude_hook_flow(args, client)

    return 0


def shutil_which(cmd: str) -> Optional[str]:
    # tiny replacement for shutil.which to avoid importing shutil everywhere
    path = os.environ.get("PATH", "")
    exts = [""]  # on windows, PATHEXT is relevant
    if os.name == "nt":
        exts = os.environ.get("PATHEXT", ".EXE;.BAT;.CMD").split(";")
    for p in path.split(os.pathsep):
        for ext in exts:
            cand = os.path.join(p, cmd + ext)
            if os.path.isfile(cand) and os.access(cand, os.X_OK):
                return cand
    return None


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except SystemExit:
        raise
    except Exception as e:
        print(f"[agentjoy] fatal: {e}", file=sys.stderr)
        traceback.print_exc()
        raise SystemExit(1)
