"""A local LangGraph approval workflow. No model calls or external actions.

Install the versions in requirements.txt. The begin and resume commands can
run in separate Python processes; SQLite keeps the paused graph state.
"""

import argparse
import json
from typing import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt


class NoticeState(TypedDict, total=False):
    subject: str
    draft: str
    approved: bool
    outcome: str


def prepare(state: NoticeState):
    subject = state["subject"].strip()
    if not subject or len(subject) > 200:
        raise ValueError("Subject must contain between 1 and 200 characters")
    return {
        "draft": f"Maintenance notice: {subject}.",
        "outcome": "awaiting_review",
    }


def review(state: NoticeState):
    decision = interrupt({
        "question": "Record this notice in the local demo state?",
        "draft": state["draft"],
    })
    if type(decision) is not bool:
        raise ValueError("The review decision must be a JSON boolean")
    return {"approved": decision}


def route_review(state: NoticeState):
    return "record" if state["approved"] else "reject"


def record(state: NoticeState):
    # This deliberately changes only graph state. It sends no notice.
    return {"outcome": "recorded_in_demo_state"}


def reject(state: NoticeState):
    return {"outcome": "rejected"}


def build_graph(checkpointer):
    builder = StateGraph(NoticeState)
    builder.add_node("prepare", prepare)
    builder.add_node("review", review)
    builder.add_node("record", record)
    builder.add_node("reject", reject)
    builder.add_edge(START, "prepare")
    builder.add_edge("prepare", "review")
    builder.add_conditional_edges(
        "review", route_review, {"record": "record", "reject": "reject"}
    )
    builder.add_edge("record", END)
    builder.add_edge("reject", END)
    return builder.compile(checkpointer=checkpointer)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("command", choices=["begin", "resume", "inspect"])
    parser.add_argument("database", help="Path to a local SQLite file")
    parser.add_argument("--thread", default="notice-1")
    parser.add_argument("--subject", default="Search will be unavailable at 18:00 UTC")
    parser.add_argument("--decision", choices=["approve", "reject"])
    args = parser.parse_args()
    config = {"configurable": {"thread_id": args.thread}}
    with SqliteSaver.from_conn_string(args.database) as saver:
        graph = build_graph(saver)
        snapshot = graph.get_state(config)
        if args.command == "begin":
            if snapshot.values:
                parser.error("Thread already exists; inspect it or use a new --thread")
            result = graph.invoke({"subject": args.subject}, config)
            print(json.dumps({"pending": result["__interrupt__"][0].value}))
        elif args.command == "resume":
            if args.decision is None:
                parser.error("resume requires --decision approve or reject")
            if not any(task.interrupts for task in snapshot.tasks):
                parser.error("Thread has no pending review")
            result = graph.invoke(Command(resume=args.decision == "approve"), config)
            print(json.dumps({"outcome": result["outcome"], "draft": result["draft"]}))
        else:
            print(json.dumps({"state": snapshot.values, "next": list(snapshot.next)}))


if __name__ == "__main__":
    main()
