"""Set up the governed-writes example as code: spec, policies, agent and approver.

Python 3.12+, standard library only. Uses Remote MCP for the integration and
REST for the agent. Two steps stay in the browser by design: entering the
Acme Orders credential and any recent-authentication approval the platform asks
for. Nothing here accepts or prints a secret.

Every step is safe to re-run; ids are kept in the state directory.
"""

import argparse
import getpass
import json
import os
from pathlib import Path
import sys
from urllib.error import HTTPError
from urllib.request import Request, urlopen
import uuid


SPEC_PATH = Path(__file__).with_name("orders-api.openapi.json")
SLUG = "acme-orders"
USER_AGENT = "dt-governed-writes-example/1.0"
CHARTER = (
    "You are the Acme refund desk. When a support message arrives, look up the order with the Acme Orders "
    "tools, add a short internal note describing what you found and what you intend to do, then issue the "
    "refund the evidence supports. Refund no more than the item price in question unless the whole order is "
    "affected. If a refund is declined, do not retry it; explain what is blocked in a note."
)


class SetupError(RuntimeError):
    pass


class Client:
    def __init__(self, args):
        self.api_base = os.environ.get("DT_API_BASE", "http://localhost:8000/api/v1").rstrip("/")
        self.origin = self.api_base.rsplit("/api/", 1)[0]
        self.api_key = os.environ.get("DT_API_KEY") or getpass.getpass("Organization API key (approver-issued): ")
        self.state_path = Path(args.state_dir) / "setup.json"
        self.state_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
        self.state = json.loads(self.state_path.read_text()) if self.state_path.exists() else {}
        self.protocol = None

    def save(self, **values):
        self.state.update(values)
        self.state_path.write_text(json.dumps(self.state, indent=2))

    def _post(self, url, payload, extra_headers=None):
        headers = {"X-Api-Key": self.api_key, "Content-Type": "application/json",
                   "Accept": "application/json, text/event-stream", "User-Agent": USER_AGENT, **(extra_headers or {})}
        request = Request(url, data=json.dumps(payload).encode(), headers=headers, method="POST")
        try:
            with urlopen(request, timeout=60) as response:
                raw = response.read()
                return json.loads(raw) if raw else {}
        except HTTPError as exc:
            raise SetupError(f"HTTP {exc.code} from {url}: {exc.read()[:400].decode(errors='replace')}") from None

    def rest(self, method, path, payload=None, idempotency_key=None):
        if method == "POST":
            headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
            return self._post(f"{self.api_base}{path}", payload or {}, headers)
        request = Request(f"{self.api_base}{path}", method=method, headers={
            "X-Api-Key": self.api_key, "Accept": "application/json", "User-Agent": USER_AGENT})
        try:
            with urlopen(request, timeout=60) as response:
                return json.loads(response.read() or b"{}")
        except HTTPError as exc:
            raise SetupError(f"HTTP {exc.code} from {path}: {exc.read()[:400].decode(errors='replace')}") from None

    def mcp(self, tool, arguments):
        if self.protocol is None:
            init = self._post(f"{self.api_base}/mcp/", {
                "jsonrpc": "2.0", "id": "init", "method": "initialize",
                "params": {"protocolVersion": "2025-11-25", "capabilities": {},
                           "clientInfo": {"name": "governed-writes-setup", "version": "1.0.0"}},
            })
            self.protocol = init["result"]["protocolVersion"]
        reply = self._post(f"{self.api_base}/mcp/", {
            "jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "tools/call",
            "params": {"name": tool, "arguments": arguments},
        }, {"MCP-Protocol-Version": self.protocol})
        if "error" in reply:
            raise SetupError(f"{tool}: {reply['error'].get('message')}")
        result = reply["result"]
        content = result.get("structuredContent")
        if content is None:
            text = "".join(part.get("text", "") for part in result.get("content", []))
            content = json.loads(text) if text.startswith("{") else {"text": text}
        if result.get("isError"):
            raise SetupError(f"{tool}: {content.get('message') or content.get('code') or content}")
        return content

    def mutate(self, tool, arguments):
        """Run a control mutation, pausing for browser approval when the platform asks for it."""
        arguments = {**arguments, "request_id": arguments.get("request_id") or str(uuid.uuid4())}
        while True:
            result = self.mcp(tool, arguments)
            if result.get("status") != "action_required":
                return result
            url = result.get("action", {}).get("url", "")
            print(f"\n  {tool} needs a human step ({result.get('code')}).")
            print(f"  Open {self.origin}{url if url.startswith('/') else '/' + url} as the key's issuer, review, approve.")
            input("  Press Enter once approved to retry the identical request... ")


def find_integration(client):
    page = client.mcp("list_integrations", {"kind": "openapi", "page_size": 100})
    for item in page["integrations"]:
        if item.get("slug") == SLUG:
            return item["id"]
    return None


def cmd_import(client, args):
    spec = SPEC_PATH.read_text()
    integration_id = client.state.get("integration_id") or find_integration(client)
    configuration = {"display_name": "Acme Orders", "description": "Demo internal orders service.",
                     "base_url": args.base_url, "auth_scheme": "bearer"}
    if integration_id:
        client.mutate("configure_openapi_integration",
                      {"action": "update", "integration_id": integration_id, "configuration": configuration})
        result = client.mutate("configure_openapi_integration",
                               {"action": "import", "integration_id": integration_id, "spec": spec})
    else:
        result = client.mutate("configure_openapi_integration",
                               {"action": "create", "slug": SLUG, "configuration": configuration, "spec": spec})
        integration_id = result["integration"]["id"]
    client.save(integration_id=integration_id)

    details = client.mcp("get_integration", {"kind": "openapi", "integration_id": integration_id, "page_size": 100})
    revision_id = details["revision"]["id"]
    operations = details["operations"]["items"]
    # Select every supported operation. Policies come from x-dt-policy in the spec,
    # so the governance decision is reviewed in the same pull request as the API.
    client.mutate("configure_openapi_integration", {
        "action": "operations", "integration_id": integration_id, "revision_id": revision_id,
        "operations": [{"id": op["id"], "selected": op["supported"]} for op in operations],
    })
    client.mutate("configure_openapi_integration",
                  {"action": "publish", "integration_id": integration_id, "revision_id": revision_id})

    print(f"\nPublished Acme Orders revision {details['revision']['number']} ({integration_id})")
    for op in operations:
        mark = op["policy"] if op["supported"] else f"unsupported: {op['unsupported_reason']}"
        print(f"  {op['method']:<5} {op['path']:<28} {op['tool_name']:<16} {mark}")
    setup = client.mcp("get_integration_setup", {"kind": "openapi", "integration_id": integration_id})
    if setup.get("status") == "action_required":
        url = setup["action"]["url"]
        print(f"\nNext: add the Acme Orders bearer token in the browser (secrets never pass through this API):")
        print(f"  {client.origin}{url}")


def cmd_agent(client, args):
    agent_id = client.state.get("agent_id")
    if agent_id:
        print(f"Agent already created: {agent_id}")
        return
    key = client.state.get("agent_create_key") or str(uuid.uuid4())
    client.save(agent_create_key=key)  # Retry with the same key after an ambiguous failure.
    agent = client.rest("POST", "/agents/", {
        "name": args.name, "charter": CHARTER, "schedule": None, "is_active": False,
    }, idempotency_key=key)
    client.save(agent_id=agent["id"])
    print(f"Created inactive agent {agent['name']}: {agent['id']}")


def key_issuer_user_id(client):
    org = client.mcp("list_organizations", {})["organizations"][0]
    members = client.mcp("list_workspace_members", {"page_size": 100})["members"]
    return next(m["user_id"] for m in members if m["id"] == org["membership_id"])


def cmd_bind(client, args):
    integration_id, agent_id = client.state.get("integration_id"), client.state.get("agent_id")
    if not integration_id or not agent_id:
        sys.exit("Run `import` and `agent` first.")
    credentials = client.mcp("list_credentials", {"integration_id": integration_id})
    usable = [c for c in credentials["credentials"] if c.get("kind") == "bearer" and not c.get("revoked_at")]
    if not usable:
        sys.exit("No Acme Orders credential yet. Add the bearer token in the browser, then re-run `bind`.")
    approver = args.approver_user_id or key_issuer_user_id(client)
    client.mutate("set_agent_integration", {
        "agent_id": agent_id, "kind": "openapi", "integration_id": integration_id, "enabled": True,
        "credential_id": usable[0]["id"], "approver_ids": [int(approver)],
    })
    print(f"Bound Acme Orders to {agent_id}; approver is user {approver}.")
    client.rest("POST", f"/agents/{agent_id}/activate/")
    print("Agent activated.")
    # Preflight checks stored configuration only; worker execution and network stay "unknown".
    readiness = client.mcp("get_agent_readiness",
                           {"agent_id": agent_id, "kind": "openapi", "integration_id": integration_id})
    print(f"Readiness: {readiness.get('status')} (configuration_ready={readiness.get('configuration_ready')})")
    for check in readiness.get("checks", []):
        if check.get("status") == "blocked":
            print(f"  {check.get('check')}: {check.get('code')}. {check.get('next_step') or ''}")
    print(f"\nexport DT_AGENT_ID={agent_id}")


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--state-dir", default=".approver-state")
    commands = parser.add_subparsers(dest="command", required=True)
    imp = commands.add_parser("import", help="Create or update the integration from the spec and publish it")
    imp.add_argument("--base-url", required=True, help="Where the platform reaches Acme Orders")
    imp.set_defaults(handler=cmd_import)
    agent = commands.add_parser("agent", help="Create the inactive refund-desk agent")
    agent.add_argument("--name", default="Acme Refund Desk")
    agent.set_defaults(handler=cmd_agent)
    bind = commands.add_parser("bind", help="Bind credential and approver, check readiness, activate")
    bind.add_argument("--approver-user-id", help="Approving member's user id (default: this key's issuer, "
                      "who must also be the person resolving approvals with approver.py)")
    bind.set_defaults(handler=cmd_bind)
    args = parser.parse_args()
    try:
        args.handler(Client(args), args)
    except SetupError as exc:
        sys.exit(f"Setup failed: {exc}")


if __name__ == "__main__":
    main()
