"""A complete local ReAct control loop. Authored fixture decisions; no LLM or HTTP."""
import json
import math


def calculate_exposure(arguments):
    if not isinstance(arguments, dict) or set(arguments) != {"position_value", "portfolio_value"}:
        raise ValueError("Expected position_value and portfolio_value")
    position, portfolio = arguments["position_value"], arguments["portfolio_value"]
    if any(isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value)
           for value in (position, portfolio)):
        raise ValueError("Values must be finite numbers")
    if position < 0 or portfolio <= 0:
        raise ValueError("Position must be nonnegative and portfolio positive")
    exposure = 100 * (position / portfolio)
    if not math.isfinite(exposure):
        raise ValueError("Exposure must be finite")
    return {"exposure_pct": exposure}


TOOLS = {"calculate_exposure": calculate_exposure}


def run_loop(decide, max_iterations=4):
    if not isinstance(max_iterations, int) or isinstance(max_iterations, bool) or not 1 <= max_iterations <= 20:
        raise ValueError("max_iterations must be an integer from 1 to 20")
    messages = [{"role": "user", "content": "Calculate the exposure of an illustrative $8,000 position in a $10,000 portfolio."}]
    tool_calls = 0
    for iteration in range(1, max_iterations + 1):
        try:
            decision = decide(list(messages))
            if not isinstance(decision, dict):
                raise ValueError("Decision must be an object")
            if decision.get("type") == "finish":
                if set(decision) != {"type", "answer"} or not isinstance(decision["answer"], str) or not decision["answer"].strip():
                    raise ValueError("Final answer must be nonempty text")
                return {"status": "completed", "iterations": iteration, "toolCalls": tool_calls, "answer": decision["answer"]}
            if set(decision) != {"type", "tool", "arguments"} or decision["type"] != "tool":
                raise ValueError("Expected a tool decision or final answer")
            name = decision["tool"]
            if not isinstance(name, str) or name not in TOOLS:
                raise ValueError("Tool is not allowed")
            observation = TOOLS[name](decision["arguments"])
            tool_calls += 1
            messages.append({"role": "assistant", "content": decision})
            messages.append({"role": "tool", "name": name, "content": observation})
        except (ValueError, TypeError, KeyError, OverflowError) as error:
            return {"status": "error", "iterations": iteration, "toolCalls": tool_calls, "error": str(error)}
    return {"status": "max_iterations_reached", "iterations": max_iterations, "toolCalls": tool_calls}


def fixture_decision(messages):
    if messages[-1]["role"] == "tool":
        exposure = messages[-1]["content"]["exposure_pct"]
        return {"type": "finish", "answer": f"Fixture exposure: {exposure:.2f}%"}
    return {"type": "tool", "tool": "calculate_exposure", "arguments": {"position_value": 8000, "portfolio_value": 10000}}


if __name__ == "__main__":
    print(json.dumps(run_loop(fixture_decision)))
