"""AgenticCalling MCP Server — give any AI agent the ability to make phone calls.

Agent-to-agent voice capability: your AI agent delegates phone calls to a
specialized voice agent that handles the live conversation. Works with Claude,
ChatGPT, OpenClaw, LangChain, CrewAI, or any MCP-compatible client.

Environment variables:
    AC_API_KEY    — Your AgenticCalling API key (for stdio/Claude Code mode)
    AC_BASE_URL   — API base URL (default: http://localhost:8000/v1 for local dev)

In SSE mode (Claude Web), authentication is handled via OAuth + email OTP.
No AC_API_KEY is needed — users authenticate through the browser.
"""
from __future__ import annotations

import contextvars
import json
import os
import time
from typing import Any

import httpx
import mcp.server.stdio
import mcp.types as types
from mcp.server import Server
from mcp.server.models import InitializationOptions

app = Server("agenticcalling")

DEFAULT_BASE_URL = "http://localhost:8000/v1"

# Per-request API key for SSE mode (set during OAuth, used by tool handlers)
_current_api_key: contextvars.ContextVar[str] = contextvars.ContextVar("_current_api_key", default="")


def _api_key() -> str:
    # SSE mode: use the per-request token from OAuth
    key = _current_api_key.get("")
    if key:
        return key
    # stdio mode: use env var
    key = os.getenv("AC_API_KEY", "")
    if not key:
        raise ValueError(
            "Not authenticated. Connect via Claude Web (SSE mode) or set AC_API_KEY."
        )
    return key


def _base_url() -> str:
    return os.getenv("AC_BASE_URL", DEFAULT_BASE_URL).rstrip("/")


def _headers() -> dict[str, str]:
    return {
        "Authorization": f"Bearer {_api_key()}",
        "Content-Type": "application/json",
    }


def _get(path: str, params: dict | None = None) -> dict:
    with httpx.Client(base_url=_base_url(), headers=_headers(), timeout=15) as c:
        r = c.get(path, params=params)
        r.raise_for_status()
        return r.json()


def _post(path: str, body: dict | None = None) -> dict:
    with httpx.Client(base_url=_base_url(), headers=_headers(), timeout=30) as c:
        r = c.post(path, json=body)
        r.raise_for_status()
        return r.json()


def _delete(path: str) -> dict:
    with httpx.Client(base_url=_base_url(), headers=_headers(), timeout=15) as c:
        r = c.delete(path)
        if r.status_code == 204:
            return {"status": "deleted"}
        r.raise_for_status()
        return r.json()


# ── Tools ────────────────────────────────────────────────────────────────────


@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="make_call",
            description=(
                "Make a real phone call to any number worldwide. Delegates to a specialized voice "
                "agent that dials, speaks, and listens in real-time — an agent-to-agent capability. "
                "Use it to book appointments, cancel reservations, negotiate prices, gather information, "
                "qualify leads, do cold outreach, follow up with customers, or anything that normally "
                "requires a human on the phone. Automatically handles voicemail, IVR menu navigation, "
                "hold music, call screening, and language switching. Returns a call_id to track progress."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "to": {
                        "type": "string",
                        "description": "Phone number in E.164 format (e.g. +14155551234)",
                    },
                    "objective": {
                        "type": "string",
                        "description": "What the AI agent should accomplish on the call",
                    },
                    "context": {
                        "type": "string",
                        "description": "Background information the agent should know before calling",
                    },
                    "voice": {
                        "type": "string",
                        "enum": ["alloy", "ash", "ballad", "coral", "echo", "sage", "shimmer", "verse"],
                        "description": "Voice for the AI agent",
                    },
                    "language": {"type": "string", "default": "en"},
                    "persona_id": {"type": "string", "description": "Use a saved persona"},
                    "playbook_id": {"type": "string", "description": "Use a saved playbook/strategy"},
                    "behavior_rules": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Hard constraints (e.g. 'Never discuss pricing')",
                    },
                    "on_voicemail": {
                        "type": "string",
                        "enum": ["leave_message", "hang_up", "retry"],
                        "default": "leave_message",
                    },
                    "max_duration_minutes": {"type": "integer", "default": 5},
                    "pipeline": {
                        "type": "string",
                        "enum": ["realtime", "pipeline"],
                        "default": "pipeline",
                        "description": "Voice pipeline: 'realtime' (lowest latency) or 'pipeline' (full transcripts, better quality)",
                    },
                },
                "required": ["to", "objective"],
            },
        ),
        types.Tool(
            name="get_call",
            description=(
                "Check the status of a call. Returns status, transcript, summary, "
                "and any structured data extracted from the conversation."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "call_id": {"type": "string", "description": "The call ID returned by make_call"},
                },
                "required": ["call_id"],
            },
        ),
        types.Tool(
            name="wait_for_call",
            description=(
                "Wait for a call to finish (polls until terminal state). "
                "Use this after make_call to get the final result."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "call_id": {"type": "string"},
                    "timeout_seconds": {"type": "integer", "default": 300, "description": "Max seconds to wait"},
                },
                "required": ["call_id"],
            },
        ),
        types.Tool(
            name="list_calls",
            description="List recent calls with their statuses.",
            inputSchema={
                "type": "object",
                "properties": {
                    "status": {
                        "type": "string",
                        "enum": [
                            "queued", "dialing", "connected", "in_progress",
                            "completed", "failed", "no_answer", "exhausted", "cancelled",
                        ],
                        "description": "Filter by status",
                    },
                    "limit": {"type": "integer", "default": 10},
                },
            },
        ),
        types.Tool(
            name="cancel_call",
            description="Cancel a queued or scheduled call before it's dialed.",
            inputSchema={
                "type": "object",
                "properties": {
                    "call_id": {"type": "string"},
                },
                "required": ["call_id"],
            },
        ),
        types.Tool(
            name="make_batch_calls",
            description=(
                "Call multiple phone numbers with the same (or per-target) objectives. "
                "Returns a batch_id and per-call status. Max 1000 targets."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "targets": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "to": {"type": "string", "description": "Phone number in E.164"},
                                "objective": {"type": "string", "description": "Override batch objective for this target"},
                                "context": {"type": "string", "description": "Per-target context"},
                            },
                            "required": ["to"],
                        },
                        "description": "List of phone numbers to call (1-1000)",
                    },
                    "objective": {
                        "type": "string",
                        "description": "Default objective for all targets",
                    },
                    "persona_id": {"type": "string"},
                    "playbook_id": {"type": "string"},
                },
                "required": ["targets", "objective"],
            },
        ),
        types.Tool(
            name="get_usage",
            description=(
                "Check current usage, costs, and billing. Shows minutes used, "
                "remaining balance, total spend with cost breakdown (Twilio + OpenAI + LiveKit + margin)."
            ),
            inputSchema={"type": "object", "properties": {}},
        ),
        types.Tool(
            name="create_persona",
            description=(
                "Create a reusable agent persona (name, voice, personality). "
                "Use the returned persona_id in future calls."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Internal name"},
                    "display_name": {"type": "string", "description": "Name the agent introduces itself as"},
                    "backstory": {"type": "string", "description": "Character backstory for the system prompt"},
                    "communication_style": {"type": "string", "description": "e.g. warm, professional, casual"},
                    "voice_provider": {
                        "type": "string",
                        "enum": ["openai", "elevenlabs"],
                        "default": "openai",
                    },
                    "language": {"type": "string", "default": "en"},
                },
                "required": ["name", "display_name"],
            },
        ),
        types.Tool(
            name="list_personas",
            description="List all saved agent personas.",
            inputSchema={"type": "object", "properties": {}},
        ),
        types.Tool(
            name="create_playbook",
            description=(
                "Create a reusable call strategy (playbook). Defines conversation approach, "
                "extraction schema, success criteria. Use the returned playbook_id in calls."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "strategy": {"type": "string", "description": "Conversation strategy in natural language"},
                    "behavior_rules": {"type": "array", "items": {"type": "string"}},
                    "extraction_schema": {
                        "type": "object",
                        "description": "JSON schema of data to extract from the conversation",
                    },
                    "success_criteria": {"type": "string"},
                    "voice_tone": {"type": "string"},
                },
                "required": ["name", "strategy"],
            },
        ),
        types.Tool(
            name="list_playbooks",
            description="List all saved playbooks.",
            inputSchema={"type": "object", "properties": {}},
        ),
        types.Tool(
            name="send_sms",
            description=(
                "Send a text message (SMS) to a phone number. "
                "Uses the org's phone line as the sender."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "to": {"type": "string", "description": "Phone number in E.164 format (e.g. +14155551234)"},
                    "message": {"type": "string", "description": "Text message body (max 1600 chars)"},
                    "call_id": {"type": "string", "description": "Optional: link SMS to a call (for follow-ups)"},
                },
                "required": ["to", "message"],
            },
        ),
        types.Tool(
            name="list_sms_threads",
            description="List SMS conversation threads with contacts.",
            inputSchema={"type": "object", "properties": {}},
        ),
        types.Tool(
            name="get_sms_thread",
            description="Get all messages in an SMS thread.",
            inputSchema={
                "type": "object",
                "properties": {
                    "thread_id": {"type": "string", "description": "Thread ID"},
                },
                "required": ["thread_id"],
            },
        ),
    ]


# ── Tool handlers ────────────────────────────────────────────────────────────

TERMINAL_STATES = {
    "completed", "failed", "exhausted", "cancelled", "busy", "no_answer",
}


def _format_call(call: dict) -> dict:
    """Build a clean result dict from a raw call API response."""
    result: dict[str, Any] = {
        "call_id": call["id"],
        "status": call["status"],
        "to": call["to_number"],
        "objective": call.get("objective", ""),
    }
    status = call["status"]

    if status == "completed":
        result["duration_seconds"] = call.get("duration_seconds")
        result["cost_usd"] = call.get("cost")
        result["summary"] = call.get("summary")
        if call.get("transcript"):
            result["transcript"] = call["transcript"]
        if call.get("structured_data"):
            result["structured_data"] = call["structured_data"]
    elif status == "failed":
        result["failure_reason"] = call.get("failure_reason")
    elif status in ("retry_scheduled", "no_answer", "voicemail_left"):
        result["attempt_number"] = call.get("attempt_number")
        result["max_attempts"] = call.get("max_attempts")

    return result


@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
    try:
        result = _dispatch(name, arguments)
        return [types.TextContent(type="text", text=json.dumps(result, indent=2, default=str))]
    except httpx.HTTPStatusError as exc:
        body = exc.response.text
        return [types.TextContent(
            type="text",
            text=f"API error {exc.response.status_code}: {body}",
        )]
    except Exception as exc:
        return [types.TextContent(type="text", text=f"Error: {exc}")]


def _dispatch(name: str, args: dict) -> dict:
    if name == "make_call":
        body: dict[str, Any] = {
            "to": args["to"],
            "objective": args["objective"],
            "language": args.get("language", "en"),
            "on_voicemail": args.get("on_voicemail", "leave_message"),
            "max_duration_minutes": args.get("max_duration_minutes", 5),
        }
        for opt in ("context", "voice", "persona_id", "playbook_id", "behavior_rules", "pipeline"):
            if args.get(opt) is not None:
                body[opt] = args[opt]

        call = _post("/calls", body)
        return {
            "call_id": call["id"],
            "status": call["status"],
            "message": (
                f"Call to {args['to']} is queued. "
                f"Use wait_for_call or get_call with call_id '{call['id']}' to track it."
            ),
        }

    elif name == "get_call":
        call = _get(f"/calls/{args['call_id']}")
        return _format_call(call)

    elif name == "wait_for_call":
        call_id = args["call_id"]
        timeout = args.get("timeout_seconds", 300)
        deadline = time.time() + timeout
        last_status = None

        while time.time() < deadline:
            call = _get(f"/calls/{call_id}")
            status = call.get("status", "unknown")
            if status != last_status:
                last_status = status
            if status in TERMINAL_STATES:
                result = _format_call(call)
                result["waited"] = True
                return result
            time.sleep(3)

        # Timed out
        call = _get(f"/calls/{call_id}")
        result = _format_call(call)
        result["waited"] = True
        result["timed_out"] = True
        result["message"] = f"Call still in state '{call['status']}' after {timeout}s. Check again later."
        return result

    elif name == "list_calls":
        params: dict[str, Any] = {"page_size": args.get("limit", 10)}
        if args.get("status"):
            params["status"] = args["status"]
        data = _get("/calls", params=params)
        # API may return list or paginated object
        calls = data if isinstance(data, list) else data.get("calls", data.get("data", [data]))
        return {
            "calls": [_format_call(c) for c in calls],
            "count": len(calls),
        }

    elif name == "cancel_call":
        call = _post(f"/calls/{args['call_id']}/cancel")
        return {"call_id": args["call_id"], "status": "cancelled"}

    elif name == "make_batch_calls":
        body: dict[str, Any] = {
            "targets": args["targets"],
            "objective": args["objective"],
        }
        for opt in ("persona_id", "playbook_id"):
            if args.get(opt):
                body[opt] = args[opt]

        result = _post("/calls/batch", body)
        return {
            "batch_id": result.get("batch_id"),
            "total": result.get("total"),
            "queued": result.get("queued"),
            "errors": result.get("errors"),
            "calls": result.get("calls", []),
            "failed": result.get("failed", []),
            "message": f"Batch created: {result.get('queued')}/{result.get('total')} calls queued",
        }

    elif name == "get_usage":
        usage = _get("/usage")
        cost = usage.get("cost", {})
        result: dict[str, Any] = {
            "plan": usage.get("plan"),
            "minutes_used": usage.get("minutes_used_this_month"),
            "minutes_limit": usage.get("minutes_limit"),
            "minutes_remaining": usage.get("minutes_remaining"),
            "sms_sent": usage.get("sms_sent_this_month"),
            "sms_limit": usage.get("sms_limit"),
            "concurrent_calls_limit": usage.get("concurrent_calls_limit"),
            "total_spend_usd": cost.get("total_spend_usd"),
            "total_calls_billed": cost.get("total_calls_billed"),
            "rate_per_minute_usd": cost.get("rate_per_minute_usd"),
            "sms_rate_per_message_usd": cost.get("sms_rate_per_message_usd"),
        }
        if usage.get("minutes_remaining") is not None and usage.get("minutes_limit"):
            pct = (usage["minutes_used_this_month"] / usage["minutes_limit"]) * 100
            result["usage_percentage"] = round(pct, 1)
        return result

    elif name == "create_persona":
        body = {k: v for k, v in args.items() if v is not None}
        persona = _post("/personas", body)
        return {
            "persona_id": persona["id"],
            "name": persona.get("display_name") or persona.get("name"),
            "message": f"Persona created. Use persona_id '{persona['id']}' in make_call.",
        }

    elif name == "list_personas":
        personas = _get("/personas")
        if isinstance(personas, list):
            return {
                "personas": [
                    {"id": p["id"], "name": p.get("display_name") or p["name"]}
                    for p in personas
                ],
                "count": len(personas),
            }
        return personas

    elif name == "create_playbook":
        body = {k: v for k, v in args.items() if v is not None}
        pb = _post("/playbooks", body)
        return {
            "playbook_id": pb["id"],
            "name": pb["name"],
            "message": f"Playbook created. Use playbook_id '{pb['id']}' in make_call.",
        }

    elif name == "list_playbooks":
        playbooks = _get("/playbooks")
        if isinstance(playbooks, list):
            return {
                "playbooks": [{"id": p["id"], "name": p["name"]} for p in playbooks],
                "count": len(playbooks),
            }
        return playbooks

    elif name == "send_sms":
        body: dict[str, Any] = {
            "to": args["to"],
            "body": args["message"],
        }
        if args.get("call_id"):
            body["call_id"] = args["call_id"]

        sms = _post("/sms/send", body)
        return {
            "sms_id": sms.get("id"),
            "status": sms.get("status"),
            "to": sms.get("to_number"),
            "from": sms.get("from_number"),
            "message": f"SMS sent to {args['to']}",
        }

    elif name == "list_sms_threads":
        threads = _get("/sms/threads")
        if isinstance(threads, list):
            return {
                "threads": [
                    {
                        "id": t["id"],
                        "contact": t["contact_number"],
                        "last_message": t.get("last_message_preview"),
                        "last_at": t.get("last_message_at"),
                        "unread": t.get("unread_count", 0),
                    }
                    for t in threads
                ],
                "count": len(threads),
            }
        return threads

    elif name == "get_sms_thread":
        data = _get(f"/sms/threads/{args['thread_id']}")
        thread = data.get("thread", {})
        messages = data.get("messages", [])
        return {
            "contact": thread.get("contact_number"),
            "messages": [
                {
                    "direction": m["direction"],
                    "body": m["body"],
                    "status": m["status"],
                    "timestamp": m.get("created_at"),
                }
                for m in messages
            ],
            "count": len(messages),
        }

    else:
        return {"error": f"Unknown tool: {name}"}


# ── Entrypoints ──────────────────────────────────────────────────────────────


async def main_stdio():
    """Run as stdio transport (for Claude Code / Claude Desktop)."""
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, _init_options())


def main_sync():
    import asyncio
    asyncio.run(main_stdio())


def _init_options() -> InitializationOptions:
    from mcp.server.lowlevel.server import NotificationOptions
    return InitializationOptions(
        server_name="agenticcalling",
        server_version="0.1.0",
        capabilities=app.get_capabilities(
            notification_options=NotificationOptions(),
            experimental_capabilities={},
        ),
    )


def create_sse_app():
    """Create a Starlette ASGI app with SSE transport and Clerk-based OAuth.

    Flow: Claude → /authorize → redirect to Clerk sign-in → Clerk callback
    → register/get API key → issue OAuth code → Claude gets Bearer token.
    """
    import secrets
    import urllib.parse
    from mcp.server.sse import SseServerTransport
    from starlette.applications import Starlette
    from starlette.routing import Mount, Route
    from starlette.requests import Request
    from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse

    sse = SseServerTransport("/messages/")

    _auth_codes: dict[str, str] = {}  # code → api_key
    # Pending OAuth sessions: nonce → {redirect_uri, state, client_id, ...}
    _pending_sessions: dict[str, dict] = {}

    CLERK_SIGN_IN_URL = os.getenv("CLERK_SIGN_IN_URL", "https://agenticcalling.ai/sign-in")
    DASHBOARD_URL = os.getenv("DASHBOARD_URL", "https://agenticcalling.ai")

    # ── SSE handler ──────────────────────────────────────────────────────

    async def handle_sse(request: Request):
        auth = request.headers.get("authorization", "")
        if auth.startswith("Bearer "):
            _current_api_key.set(auth[7:])
        async with sse.connect_sse(
            request.scope, request.receive, request._send
        ) as (read_stream, write_stream):
            await app.run(read_stream, write_stream, _init_options())

    # ── Health ───────────────────────────────────────────────────────────

    async def handle_health(request: Request):
        return JSONResponse({"status": "ok", "server": "agenticcalling-mcp"})

    # ── OAuth 2.0 discovery ──────────────────────────────────────────────

    def _server_url(request: Request) -> str:
        proto = request.headers.get("x-forwarded-proto", request.url.scheme)
        host = request.headers.get("x-forwarded-host", request.url.netloc)
        return f"{proto}://{host}"

    async def oauth_protected_resource(request: Request):
        base = _server_url(request)
        return JSONResponse({
            "resource": f"{base}/sse",
            "authorization_servers": [f"{base}/"],
            "bearer_methods_supported": ["header"],
        })

    async def oauth_authorization_server(request: Request):
        base = _server_url(request)
        return JSONResponse({
            "issuer": f"{base}/",
            "authorization_endpoint": f"{base}/authorize",
            "token_endpoint": f"{base}/token",
            "registration_endpoint": f"{base}/register",
            "response_types_supported": ["code"],
            "grant_types_supported": ["authorization_code"],
            "token_endpoint_auth_methods_supported": ["none"],
            "code_challenge_methods_supported": ["S256"],
        })

    async def oauth_register(request: Request):
        body = await request.json()
        client_id = f"client_{secrets.token_hex(8)}"
        return JSONResponse({
            "client_id": client_id,
            "client_name": body.get("client_name", "Claude"),
            "redirect_uris": body.get("redirect_uris", []),
            "grant_types": ["authorization_code"],
            "response_types": ["code"],
            "token_endpoint_auth_method": "none",
        }, status_code=201)

    # ── OAuth authorize → redirect to Clerk sign-in ──────────────────────

    async def oauth_authorize(request: Request):
        redirect_uri = request.query_params.get("redirect_uri", "")
        state = request.query_params.get("state", "")
        client_id = request.query_params.get("client_id", "")
        code_challenge = request.query_params.get("code_challenge", "")
        code_challenge_method = request.query_params.get("code_challenge_method", "")

        # Store the OAuth session so we can resume after Clerk auth
        nonce = secrets.token_urlsafe(32)
        _pending_sessions[nonce] = {
            "redirect_uri": redirect_uri,
            "state": state,
            "client_id": client_id,
            "code_challenge": code_challenge,
            "code_challenge_method": code_challenge_method,
        }

        # Show a page that redirects to Clerk sign-in, then comes back to /callback
        base = _server_url(request)
        callback_url = f"{base}/callback?nonce={urllib.parse.quote(nonce)}"

        # Redirect to the AgenticCalling dashboard sign-in
        # After Clerk sign-in, user lands on the dashboard which shows their API key
        # We use an intermediate page that asks user to paste their API key
        return HTMLResponse(f"""<!DOCTYPE html><html><head><title>Connect AgenticCalling</title>
        <style>
          * {{ box-sizing: border-box; margin: 0; padding: 0; }}
          body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                 background: #fff; color: #111; display: flex; justify-content: center;
                 align-items: center; min-height: 100vh; }}
          .card {{ border: 1px solid #e5e7eb; padding: 40px; width: 440px; max-width: 90vw; }}
          h1 {{ font-size: 20px; margin-bottom: 8px; font-weight: 600; }}
          p {{ font-size: 14px; color: #6b7280; margin-bottom: 20px; line-height: 1.6; }}
          .steps {{ margin-bottom: 24px; }}
          .step {{ display: flex; gap: 12px; margin-bottom: 16px; }}
          .step-num {{ width: 24px; height: 24px; background: #4f46e5; color: #fff; border-radius: 50%;
                       display: flex; align-items: center; justify-content: center; font-size: 12px;
                       font-weight: 700; flex-shrink: 0; }}
          .step-text {{ font-size: 14px; color: #374151; }}
          a.btn {{ display: block; width: 100%; padding: 12px; border: none; background: #4f46e5;
                   color: #fff; font-size: 15px; font-weight: 600; cursor: pointer; text-align: center;
                   text-decoration: none; margin-bottom: 16px; }}
          a.btn:hover {{ background: #4338ca; }}
          input {{ width: 100%; padding: 12px; border: 1px solid #d1d5db; font-size: 14px;
                   font-family: monospace; margin-bottom: 12px; outline: none; }}
          input:focus {{ border-color: #4f46e5; }}
          button {{ width: 100%; padding: 12px; border: 1px solid #d1d5db; background: #fff;
                    font-size: 14px; font-weight: 600; cursor: pointer; }}
          button:hover {{ background: #f9fafb; }}
          .brand {{ font-size: 12px; color: #9ca3af; text-align: center; margin-top: 24px; }}
        </style></head>
        <body><div class="card">
          <h1>Connect AgenticCalling</h1>
          <p>Give your AI the power to make real phone calls.</p>
          <div class="steps">
            <div class="step">
              <span class="step-num">1</span>
              <span class="step-text">Sign up or sign in to get your API key</span>
            </div>
            <div class="step">
              <span class="step-num">2</span>
              <span class="step-text">Copy your API key from the dashboard</span>
            </div>
            <div class="step">
              <span class="step-num">3</span>
              <span class="step-text">Paste it below to connect</span>
            </div>
          </div>
          <a class="btn" href="{DASHBOARD_URL}/sign-up" target="_blank">Sign Up / Sign In →</a>
          <form method="POST" action="/callback">
            <input type="text" name="api_key" placeholder="ac_live_..." required />
            <input type="hidden" name="nonce" value="{nonce}" />
            <button type="submit">Connect</button>
          </form>
          <div class="brand">agenticcalling.ai</div>
        </div></body></html>""")

    # ── Callback: validate API key and issue OAuth code ──────────────────

    async def oauth_callback(request: Request):
        form = await request.form()
        api_key = str(form.get("api_key", "")).strip()
        nonce = str(form.get("nonce", "")).strip()

        if nonce not in _pending_sessions:
            return HTMLResponse("<h1>Session expired. Please try connecting again.</h1>", status_code=400)

        session = _pending_sessions.pop(nonce)

        if not api_key or not api_key.startswith("ac_live_"):
            # Re-show the page with error
            _pending_sessions[nonce] = session  # restore session
            return HTMLResponse(f"""<!DOCTYPE html><html><head><title>Invalid Key</title>
            <style>body {{ font-family: sans-serif; display: flex; justify-content: center;
            align-items: center; min-height: 100vh; }} .card {{ padding: 40px; max-width: 400px;
            text-align: center; }} .error {{ color: #dc2626; margin-bottom: 16px; }}
            a {{ color: #4f46e5; }}</style></head>
            <body><div class="card">
              <p class="error">Invalid API key. It should start with ac_live_</p>
              <a href="javascript:history.back()">Go back and try again</a>
            </div></body></html>""", status_code=400)

        # Validate the API key is real by hitting the usage endpoint
        try:
            async with httpx.AsyncClient(timeout=10) as client:
                resp = await client.get(
                    f"{_base_url()}/usage",
                    headers={"Authorization": f"Bearer {api_key}"},
                )
                resp.raise_for_status()
        except Exception:
            _pending_sessions[nonce] = session  # restore session
            return HTMLResponse(f"""<!DOCTYPE html><html><head><title>Invalid Key</title>
            <style>body {{ font-family: sans-serif; display: flex; justify-content: center;
            align-items: center; min-height: 100vh; }} .card {{ padding: 40px; max-width: 400px;
            text-align: center; }} .error {{ color: #dc2626; margin-bottom: 16px; }}
            a {{ color: #4f46e5; }}</style></head>
            <body><div class="card">
              <p class="error">Could not verify this API key. Please check it and try again.</p>
              <a href="javascript:history.back()">Go back</a>
            </div></body></html>""", status_code=400)

        # Key is valid — issue OAuth code
        code = secrets.token_urlsafe(32)
        _auth_codes[code] = api_key

        redirect_uri = session["redirect_uri"]
        state = session["state"]
        sep = "&" if "?" in redirect_uri else "?"
        return RedirectResponse(
            url=f"{redirect_uri}{sep}code={code}&state={state}",
            status_code=302,
        )

    # ── OAuth token exchange ─────────────────────────────────────────────

    async def oauth_token(request: Request):
        form = await request.form()
        code = str(form.get("code", ""))

        if code not in _auth_codes:
            return JSONResponse({"error": "invalid_grant"}, status_code=400)

        api_key = _auth_codes.pop(code)
        return JSONResponse({
            "access_token": api_key,
            "token_type": "bearer",
            "expires_in": 86400 * 365,
        })

    # ── Twilio webhook proxies ───────────────────────────────────────────

    async def twilio_status_proxy(request: Request):
        body = await request.body()
        try:
            async with httpx.AsyncClient(timeout=15) as client:
                resp = await client.post(
                    f"{_base_url().replace('/v1', '')}/webhooks/twilio/status",
                    content=body,
                    headers={"Content-Type": request.headers.get("content-type", "application/x-www-form-urlencoded")},
                )
            try:
                return JSONResponse(resp.json(), status_code=resp.status_code)
            except Exception:
                from starlette.responses import Response as RawResp
                return RawResp(content=resp.content, status_code=resp.status_code)
        except Exception:
            return JSONResponse({"status": "ok"}, status_code=200)

    async def twilio_voice_proxy(request: Request):
        body = await request.body()
        async with httpx.AsyncClient(timeout=15) as client:
            resp = await client.post(
                f"{_base_url().replace('/v1', '')}/webhooks/twilio/voice",
                content=body,
                headers={"Content-Type": request.headers.get("content-type", "application/x-www-form-urlencoded")},
            )
        from starlette.responses import Response as RawResponse
        return RawResponse(content=resp.content, media_type="application/xml", status_code=resp.status_code)

    # ── Build app ────────────────────────────────────────────────────────

    starlette_app = Starlette(
        routes=[
            # MCP
            Route("/sse", handle_sse),
            Mount("/messages/", app=sse.handle_post_message),
            # OAuth discovery
            Route("/.well-known/oauth-protected-resource", oauth_protected_resource),
            Route("/.well-known/oauth-protected-resource/sse", oauth_protected_resource),
            Route("/.well-known/oauth-authorization-server", oauth_authorization_server),
            # OAuth flow
            Route("/register", oauth_register, methods=["POST"]),
            Route("/authorize", oauth_authorize, methods=["GET"]),
            Route("/callback", oauth_callback, methods=["POST"]),
            Route("/token", oauth_token, methods=["POST"]),
            # Twilio webhook proxies
            Route("/webhooks/twilio/status", twilio_status_proxy, methods=["POST"]),
            Route("/webhooks/twilio/voice", twilio_voice_proxy, methods=["POST", "GET"]),
            # Health
            Route("/health", handle_health),
        ],
    )
    return starlette_app


def main_sse():
    """Run as SSE HTTP server (for Claude web). Use: python -m agenticcalling_mcp.server --sse"""
    import uvicorn
    port = int(os.getenv("MCP_PORT", "8100"))
    print(f"\n  AgenticCalling MCP Server (SSE)")
    print(f"  Listening on http://localhost:{port}")
    print(f"  SSE endpoint: http://localhost:{port}/sse")
    print(f"\n  Next steps:")
    print(f"    1. Run: ngrok http {port}")
    print(f"    2. Copy the https://xxxx.ngrok.io URL")
    print(f"    3. In Claude web → Settings → Integrations → Add MCP")
    print(f"       URL: https://xxxx.ngrok.io/sse")
    print()
    uvicorn.run(create_sse_app(), host="0.0.0.0", port=port, log_level="info")


if __name__ == "__main__":
    import sys
    if "--sse" in sys.argv:
        main_sse()
    else:
        main_sync()
