"""Bounded tool-calling exercise. Default: deterministic fixture, no API calls.
Python 3.10+ standard library. --live uses OPENAI_API_KEY and OPENAI_MODEL.
All order records below are synthetic teaching data, not customer records.
"""
import argparse
import json
import os
import urllib.request

ORDERS = {"DEMO-1001": {"status": "shipped", "source": "synthetic_fixture"}}
TOOLS = [{"type": "function", "name": "lookup_order",
          "description": "Read a synthetic demo order status by its exact ID.",
          "strict": True, "parameters": {"type": "object", "properties": {
              "order_id": {"type": "string"}}, "required": ["order_id"],
              "additionalProperties": False}}]


def execute_tool(name, arguments):
    """Allowlisted dispatch; model output never becomes executable code."""
    if name != "lookup_order":
        return {"ok": False, "error": "unknown_tool"}
    try:
        args = json.loads(arguments)
    except (ValueError, TypeError):
        return {"ok": False, "error": "invalid_json"}
    if (not isinstance(args, dict) or set(args) != {"order_id"}
            or not isinstance(args["order_id"], str)):
        return {"ok": False, "error": "invalid_arguments"}
    order = ORDERS.get(args["order_id"])
    if order is None:
        return {"ok": False, "error": "order_not_found"}
    return {"ok": True, "order_id": args["order_id"], **order}


def run_agent(model, question, max_steps=4):
    """model(history) returns Responses-style output items; injectable in tests."""
    history = [{"role": "user", "content": question}]
    for step in range(max_steps):
        output = model(history)
        history.extend(output)  # Preserve reasoning items as well as tool calls.
        calls = [item for item in output if item.get("type") == "function_call"]
        if not calls:
            answer = "\n".join(part["text"] for item in output
                               if item.get("type") == "message"
                               for part in item.get("content", [])
                               if part.get("type") == "output_text")
            if not answer:
                raise RuntimeError("model_returned_no_answer")
            return {"answer": answer, "steps": step + 1, "history": history}
        # One read-only tool call per turn. Bound execution even for bad model output.
        if len(calls) != 1:
            raise RuntimeError("too_many_tool_calls")
        call = calls[0]
        result = execute_tool(call.get("name"), call.get("arguments"))
        history.append({"type": "function_call_output", "call_id": call["call_id"],
                        "output": json.dumps(result)})
    raise RuntimeError("step_limit_reached")


def fixture_model(history):
    """Scripted model substitute: tests the loop, not LLM reasoning quality."""
    if not history or history[-1].get("type") != "function_call_output":
        return [{"type": "function_call", "call_id": "demo-call-1",
                 "name": "lookup_order", "arguments": '{"order_id":"DEMO-1001"}'}]
    result = json.loads(history[-1]["output"])
    text = (f"{result['order_id']}: {result['status']} (synthetic_fixture)"
            if result["ok"] else f"Lookup failed: {result['error']}")
    return [{"type": "message", "role": "assistant",
             "content": [{"type": "output_text", "text": text}]}]


def live_model(history):
    key, model = os.environ.get("OPENAI_API_KEY"), os.environ.get("OPENAI_MODEL")
    if not key or not model:
        raise RuntimeError("Set OPENAI_API_KEY and OPENAI_MODEL before --live")
    payload = {"model": model, "input": history, "tools": TOOLS,
               "parallel_tool_calls": False, "store": False,
               "instructions": "Use lookup_order for order status. All data is synthetic. "
               "Never invent an order status when the tool returns an error."}
    request = urllib.request.Request(
        "https://api.openai.com/v1/responses", data=json.dumps(payload).encode(),
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
    # No automatic retry: authentication, limits and timeouts surface to the caller.
    with urllib.request.urlopen(request, timeout=20) as response:
        return json.load(response)["output"]


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--live", action="store_true")
    parser.add_argument("--question", default="Look up DEMO-1001. Do not guess its status.")
    args = parser.parse_args()
    result = run_agent(live_model if args.live else fixture_model, args.question)
    print(json.dumps({"mode": "live" if args.live else "fixture",
                      "answer": result["answer"], "steps": result["steps"]}, ensure_ascii=False))
