"""All-definition compiler. Display definitions never become invented game logic.

Every source objective becomes a separately traceable guarded operation node.
The current objective program decides active branches; array order alone cannot
prove precedence or an AND/OR relationship between objectives in the same step.
"""
from collections import Counter
from copy import deepcopy
import hashlib
import json
from .paths import root


HINT_OPERATIONS = {"hold": "inventory.hold", "deliver": "objective.deliver",
    "talk": "dialogue.talk", "report_to_npc": "dialogue.report", "move": "objective.move",
    "inspect": "objective.inspect", "inspect_or_interact": "objective.inspect", "craft": "life.craft", "alter": "life.alter",
    "dungeon_clear": "objective.dungeon_clear", "read_story": "objective.read_story",
    "gather": "life.gather", "unclassified": "objective.unresolved"}


def load_catalog():
    return json.loads((root() / "data/arbeit-catalog.json").read_text(encoding="utf8"))


def digest(value):
    return hashlib.sha256(json.dumps(value, sort_keys=True, ensure_ascii=False).encode()).hexdigest()


def compile_quest(quest):
    quest_id = quest["id"]
    nodes = []
    def add(local_id, op, params=None, *, step_id=None, objective_index=None, source_refs=None, requires=()):
        node = {"id": f"q{quest_id}/{local_id}", "op": op, "quest_id": quest_id,
            "quest_title": quest["title"], "step_id": step_id, "objective_index": objective_index,
            "params": params or {}, "source_refs": source_refs or quest["sources"],
            "requires": list(requires), "transition": "current_effect_required"}
        nodes.append(node)
        return node["id"]
    add("context", "context.identify", {"requires_field_hud": True})
    add("offer", "quest.offer", {"name": quest["name"], "title": quest["title"],
        "static_acceptance_candidate": quest["acceptance_npc"], "conditions": quest["availability"]},
        requires=("current_offer_exact_identity", "current_eligibility", "current_giver"))
    add("giver", "move.target", {"target_binding": "current_offer.giver", "role": "acceptance"},
        requires=("current_offer.giver", "current_map_transform"))
    add("accept", "dialogue.accept", {"target_id": "dialogue.accept", "binding": "current_offer"},
        requires=("current_owned_npc", "exact_offered_quest", "current_condition_branch", "source_ui_parent"))
    add("accepted", "quest.accepted", requires=("current_accepted_quest_proof",))
    groups = []
    objective_count = 0
    for step in quest["steps"]:
        members = []
        for index, objective in enumerate(step["objectives"]):
            objective_count += 1
            # A newly discovered display hint cannot quietly fall through to a
            # previously coded action. It gets its own explicit unresolved node.
            hint = objective.get("kind", "unclassified")
            op = HINT_OPERATIONS.get(hint, "objective.unresolved")
            source_ref = objective.get("source_ref") or {"asset": "QDBL/QDBLPreloadData.sjon.bin",
                "quest_id": quest_id, "step_id": step["id"], "objective_index": index,
                "note": "catalog structural path; exact source pointer retained when available"}
            params = {"objective": deepcopy(objective), "targets": deepcopy(objective.get("targets", [])),
                      "display_hint": hint, "objective_counter_goal": objective.get("goal"),
                      "runtime_semantics": (objective.get("typed_descriptor") or {}).get("runtime_action_type"),
                      "semantic_status": "needs_current_typed_or_exact_objective_binding"}
            # Preserve quantities as source counters; the operation must bind
            # exact item semantics before it can use a counter as item quantity.
            nid = add(f"step{step['id']}/objective{index}", op, params, step_id=step["id"], objective_index=index,
                      source_refs=[source_ref], requires=("current_active_objective", "exact_source_target_binding", "current_action_semantics"))
            members.append(nid)
        gate = add(f"step{step['id']}/effect", "quest.step_complete", {"members": members}, step_id=step["id"],
                   requires=("current_step_completion_or_transition",))
        groups.append({"step_id": step["id"], "display_index": step["index"], "members": members,
            "gate": gate, "precedence": "runtime_program_not_in_preload", "branch_rule": None})
    add("reward", "reward.claim", {"target_id": "reward.confirm", "reward_group": quest["reward_group"]},
        requires=("exact_quest_reward_panel", "current_reward_branch", "source_ui_parent"))
    add("completion", "quest.completed", requires=("owned_quest_completion_proof", "reward_receipt"))
    return {"schema": "masukja.arbeit-graph.v1", "quest_id": quest_id, "title": quest["title"],
        "static_name": quest["name"], "source_fingerprint": digest(quest), "nodes": nodes,
        "step_groups": groups, "objective_count": objective_count,
        "execution_policy": "Guarded operation selection by current objective program; never infer array-order game branches.",
        "static_program_complete": all((o.get("typed_descriptor") or {}).get("runtime_action_type")
            for s in quest["steps"] for o in s["objectives"]),
        "game_tested": False}


def compile_all(catalog=None):
    catalog = catalog or load_catalog()
    ids = [q["id"] for q in catalog["quests"]]
    if len(ids) != len(set(ids)):
        raise ValueError("Duplicate quest IDs in source catalog")
    graphs = [compile_quest(q) for q in catalog["quests"]]
    return {"schema": "masukja.arbeit-graphs.v1", "source_fingerprint": digest(catalog),
        "denominator": {"observed_definitions": len(graphs),
            "observed_objectives": sum(g["objective_count"] for g in graphs),
            "fixed_total_assumption": False, "unknown_server_program_branches": None},
        "graphs": graphs}


def audit(bundle=None):
    bundle = bundle or compile_all()
    from .arbeit_operations import plan_operation
    counts, gaps, records = Counter(), [], []
    for graph in bundle["graphs"]:
        ids = [n["id"] for n in graph["nodes"]]
        if len(ids) != len(set(ids)):
            raise ValueError("Duplicate node id")
        objective_nodes = [n for n in graph["nodes"] if n["objective_index"] is not None]
        if len(objective_nodes) != graph["objective_count"]:
            raise ValueError("Objective omitted from compiled graph")
        for node in graph["nodes"]:
            counts[node["op"]] += 1
            if not node["source_refs"]:
                raise ValueError("Source-free graph node")
            # An empty current frame MUST be a named boundary, never permission.
            try:
                result = plan_operation(node, {}, None)
                reason = result.get("reason") or result.get("wait_reason")
                status = result.get("status")
            except Exception as exc:
                status, reason = "integration_error", type(exc).__name__ + ": " + str(exc)
            row = {"quest_id": graph["quest_id"], "node_id": node["id"], "op": node["op"],
                   "empty_observation_result": status, "reason": reason,
                   "source_refs_present": bool(node["source_refs"]), "game_verified": False}
            records.append(row)
            if status in ("action", "complete", "integration_error"):
                gaps.append(row)
    return {"denominator": bundle["denominator"], "compiled_graphs": len(bundle["graphs"]),
        "operator_node_counts": dict(sorted(counts.items())), "nodes": records,
        "unsafe_or_unhandled_empty_frame_results": gaps,
        "all_definition_representation_passed": not gaps,
        "live_test_ready": False,
        "meaning": "Representation and fail-closed integration audit, not proof that all game routes are executable."}
