#!/usr/bin/env python3
"""odysseus-mcp — shell wrapper for MCP (Model Context Protocol) servers.

MCP servers are configured in the `mcp_servers` table. The web app's
McpManager handles live connections; this CLI manages the *config*
(read, add, enable/disable, delete). Runtime connection state lives
in the manager process — query it via the web UI or `curl /api/mcp/servers`.

    odysseus-mcp list                    # configured servers
    odysseus-mcp show SERVER_ID
    odysseus-mcp enable SERVER_ID
    odysseus-mcp disable SERVER_ID
    odysseus-mcp add --name "Filesystem" --transport stdio \\
                    --command npx --args '["@modelcontextprotocol/server-filesystem","/tmp"]'
    odysseus-mcp delete SERVER_ID
"""

from __future__ import annotations
import sys
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
quiet_logs()

import argparse, json, logging, os, sys, uuid
from pathlib import Path

try:
    from core.database import SessionLocal, McpServer
    quiet_logs()
except ModuleNotFoundError as e:
    sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n")
    sys.exit(2)


def _json_list(raw) -> list:
    try:
        value = json.loads(raw) if raw else []
    except (TypeError, json.JSONDecodeError):
        return []
    return value if isinstance(value, list) else []


def _json_dict(raw) -> dict:
    try:
        value = json.loads(raw) if raw else {}
    except (TypeError, json.JSONDecodeError):
        return {}
    return value if isinstance(value, dict) else {}


def _serialize(s: "McpServer", redact_env: bool = True) -> dict:
    args_arr = _json_list(s.args)
    env_obj = _json_dict(s.env)
    if redact_env and isinstance(env_obj, dict):
        env_obj = {k: ("***" if v else "") for k, v in env_obj.items()}
    return {
        "id": s.id,
        "name": s.name,
        "transport": s.transport,
        "command": s.command or "",
        "args": args_arr,
        "env": env_obj,
        "url": s.url or "",
        "is_enabled": bool(s.is_enabled),
        "has_oauth": bool(s.oauth_config),
        "created_at": s.created_at.isoformat() if s.created_at else "",
    }


def cmd_list(args):
    db = SessionLocal()
    try:
        rows = db.query(McpServer).order_by(McpServer.name.asc()).all()
        emit([_serialize(s) for s in rows], args)
    finally:
        db.close()


def cmd_show(args):
    db = SessionLocal()
    try:
        s = db.get(McpServer, args.id)
        if not s:
            fail(f"no MCP server with id {args.id!r}")
        # show shows env keys; values still redacted unless --reveal
        out = _serialize(s, redact_env=not args.reveal)
        emit(out, args)
    finally:
        db.close()


def cmd_enable(args):
    _set_enabled(args.id, True, args)


def cmd_disable(args):
    _set_enabled(args.id, False, args)


def _set_enabled(server_id: str, enabled: bool, args):
    db = SessionLocal()
    try:
        s = db.get(McpServer, server_id)
        if not s:
            fail(f"no MCP server with id {server_id!r}")
        s.is_enabled = enabled
        db.commit()
        emit({"ok": True, "id": s.id, "is_enabled": s.is_enabled}, args)
    finally:
        db.close()


def cmd_add(args):
    if args.transport == "stdio" and not args.command:
        fail("--command is required for stdio transport")
    if args.transport == "sse" and not args.url:
        fail("--url is required for sse transport")
    try:
        args_arr = json.loads(args.args) if args.args else []
        if not isinstance(args_arr, list):
            raise ValueError("--args must be a JSON array")
    except (json.JSONDecodeError, ValueError) as e:
        fail(f"invalid --args: {e}")
    try:
        env_obj = json.loads(args.env) if args.env else {}
        if not isinstance(env_obj, dict):
            raise ValueError("--env must be a JSON object")
    except (json.JSONDecodeError, ValueError) as e:
        fail(f"invalid --env: {e}")

    db = SessionLocal()
    try:
        s = McpServer(
            id=str(uuid.uuid4()),
            name=args.name,
            transport=args.transport,
            command=args.command or None,
            args=json.dumps(args_arr) if args_arr else None,
            env=json.dumps(env_obj) if env_obj else None,
            url=args.url or None,
            is_enabled=not args.disabled,
        )
        db.add(s)
        db.commit()
        db.refresh(s)
        emit(_serialize(s), args)
    finally:
        db.close()


def cmd_delete(args):
    db = SessionLocal()
    try:
        s = db.get(McpServer, args.id)
        if not s:
            fail(f"no MCP server with id {args.id!r}")
        snap = _serialize(s)
        db.delete(s)
        db.commit()
        emit({"ok": True, "deleted": snap}, args)
    finally:
        db.close()


def _build_parser():
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--pretty", action="store_true")
    p = argparse.ArgumentParser(prog="odysseus-mcp", parents=[common])
    sub = p.add_subparsers(dest="cmd", required=True)

    pl = sub.add_parser("list", parents=[common])
    pl.set_defaults(func=cmd_list)

    psh = sub.add_parser("show", parents=[common])
    psh.add_argument("id")
    psh.add_argument("--reveal", action="store_true", help="show env values unredacted")
    psh.set_defaults(func=cmd_show)

    pe = sub.add_parser("enable", parents=[common])
    pe.add_argument("id")
    pe.set_defaults(func=cmd_enable)

    pdis = sub.add_parser("disable", parents=[common])
    pdis.add_argument("id")
    pdis.set_defaults(func=cmd_disable)

    pa = sub.add_parser("add", parents=[common])
    pa.add_argument("--name", required=True)
    pa.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
    pa.add_argument("--command", help="stdio: executable to run")
    pa.add_argument("--args", help="JSON array of args, e.g. '[\"--root\",\"/tmp\"]'")
    pa.add_argument("--env", help="JSON object of env vars, e.g. '{\"KEY\":\"value\"}'")
    pa.add_argument("--url", help="sse: server URL")
    pa.add_argument("--disabled", action="store_true", help="create disabled")
    pa.set_defaults(func=cmd_add)

    pd = sub.add_parser("delete", parents=[common])
    pd.add_argument("id")
    pd.set_defaults(func=cmd_delete)

    return p


if __name__ == "__main__":
    sys.exit(run(_build_parser()))
