"""Evidence-driven Arbeit coordinator. No game API or pixel positions are invented.

An observation boundary follows a meaningful state transition, not each click.
Live observations must come from connected providers. Offline frames exercise
the same coordinator but cannot result in a live completion or live dispatch.
"""
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
import hashlib
import json
import os
import uuid


def utcnow():
    return datetime.now(timezone.utc).isoformat()


def stamp(value):
    parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if parsed.tzinfo is None:
        raise ValueError("Observation timestamp must include timezone")
    return parsed


PHASES = ("identify", "travel_to_npc", "accept", "objective", "return_to_npc",
          "report", "deliver", "reward", "completion", "done")


@dataclass(frozen=True)
class Observation:
    sequence: int
    observed_at: str
    character_id: str
    session_id: str
    source: str
    facts: dict = field(default_factory=dict)
    mode: str = "offline-contract"
    certainty: str = "confirmed"
    uncertainty: str = ""

    def __post_init__(self):
        stamp(self.observed_at)
        if self.sequence < 1 or not self.source or not self.session_id:
            raise ValueError("Observation requires monotonic sequence, source, and session")


class Journal:
    """Atomic durable journal + nonblocking process lock. Never overwrite a run."""
    def __init__(self, path, *, resume=False):
        self.path = Path(path)
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.lock = self.path.with_suffix(self.path.suffix + ".lock").open("a+b")
        if self.lock.tell() == 0:
            self.lock.write(b"0")
            self.lock.flush()
        self.lock.seek(0)
        try:
            if os.name == "nt":
                import msvcrt
                msvcrt.locking(self.lock.fileno(), msvcrt.LK_NBLCK, 1)
            else:
                import fcntl
                fcntl.flock(self.lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except OSError:
            self.lock.close()
            raise RuntimeError("run_already_owned")
        if self.path.exists() != resume:
            self.close()
            raise ValueError("Use resume for an existing journal; new runs need a new path")

    def write(self, data):
        tmp = self.path.with_suffix(self.path.suffix + ".new")
        with tmp.open("w", encoding="utf8") as out:
            json.dump(data, out, ensure_ascii=False, indent=2)
            out.flush()
            os.fsync(out.fileno())
        os.replace(tmp, self.path)

    def close(self):
        if not self.lock.closed:
            self.lock.close()


class ArbeitExecutor:
    """Small reusable linear lifecycle contract; full product uses GraphExecutor."""
    def __init__(self, path, *, goal=None, mode="offline-contract", resume=False):
        if mode not in ("offline-contract", "live"):
            raise ValueError("Unknown execution mode")
        self.journal = Journal(path, resume=resume)
        if resume:
            self.data = json.loads(self.journal.path.read_text(encoding="utf8"))
            if self.data["mode"] != mode or self.data.get("schema") != 1:
                self.journal.close()
                raise ValueError("Journal mode/schema mismatch")
            # Restart is not permission to send. Pending intent remains intact.
            if self.data["phase"] != "done":
                self.data["status"] = "paused"
                self.data["reason"] = "restart_requires_explicit_resume"
        else:
            if not goal or not goal.get("quest_id") or not goal.get("npc_id"):
                self.journal.close()
                raise ValueError("Explicit source-bound quest goal required")
            self.data = {"schema": 1, "run_id": uuid.uuid4().hex, "mode": mode,
                "goal": goal.copy(), "phase": "identify", "status": "paused",
                "reason": "explicit_start_required", "character_id": None, "session_id": None,
                "last_sequence": 0, "last_observed_at": None, "pending": None,
                "operations": [], "events": [], "proofs": {}, "recovery": None}
        self.save()

    def save(self):
        self.journal.write(self.data)

    def event(self, kind, reason, **details):
        self.data["events"].append({"at": utcnow(), "kind": kind, "reason": reason, **details})

    def start(self, *, explicit=False, blockers=()):
        if not explicit:
            return self.wait("explicit_start_required")
        if self.data["phase"] == "done":
            return self.report()
        if blockers:
            self.data["status"] = "blocked"
            self.data["reason"] = "preflight_incomplete"
            self.event("blocked", "preflight_incomplete", blockers=list(blockers))
        else:
            self.data["status"], self.data["reason"] = "running", "explicit_start"
            self.event("start", "explicit_start")
        self.save()
        return self.report()

    def stop(self, reason="user_stop"):
        if self.data["phase"] == "done":
            return self.report()
        self.data["status"], self.data["reason"] = "paused", reason
        self.event("pause", reason)
        self.save()
        return self.report()

    def wait(self, reason, **details):
        # A named boundary returns to caller. There is no background polling loop.
        if self.data["status"] not in ("paused", "blocked", "complete", "offline_contract_passed"):
            self.data["status"] = "waiting"
        self.data["reason"] = reason
        self.event("wait", reason, **details)
        self.save()
        return self.report()

    def report(self):
        return {k: self.data[k] for k in ("run_id", "mode", "phase", "status", "reason", "character_id", "pending", "recovery")}

    def goal_matches(self, facts):
        goal = self.data["goal"]
        return facts.get("quest_id") == goal["quest_id"] and facts.get("npc_id") == goal["npc_id"]

    def accept(self, observation):
        """Consume current evidence; absence from HUD never proves completion."""
        d = self.data
        if d["status"] in ("paused", "blocked", "complete", "offline_contract_passed"):
            return self.report()
        if observation.mode != d["mode"]:
            return self.wait("observation_mode_mismatch")
        if d["mode"] == "live" and not -.5 <= (datetime.now(timezone.utc)-stamp(observation.observed_at)).total_seconds() <= 5:
            return self.wait("live_observation_not_current")
        if observation.certainty != "confirmed" or observation.uncertainty:
            return self.wait("observation_uncertain", detail=observation.uncertainty)
        if not observation.character_id:
            return self.wait("current_character_unavailable")
        if d["character_id"] and (observation.character_id != d["character_id"] or observation.session_id != d["session_id"]):
            return self.stop("character_or_session_changed")
        if observation.sequence <= d["last_sequence"] or (d["last_observed_at"] and stamp(observation.observed_at) <= stamp(d["last_observed_at"])):
            return self.wait("stale_or_repeated_observation")
        d["last_sequence"], d["last_observed_at"] = observation.sequence, observation.observed_at
        d["character_id"], d["session_id"] = observation.character_id, observation.session_id
        f, p = observation.facts, d["phase"]
        proof = {"source": observation.source, "observed_at": observation.observed_at,
                 "sequence": observation.sequence, "character_id": observation.character_id,
                 "session_id": observation.session_id, "facts": f}
        pending = d["pending"]
        if pending and pending.get("intermediate_effects"):
            expected = pending["intermediate_effects"]
            if (observation.sequence > pending["after_sequence"] and
                    stamp(observation.observed_at) > stamp(pending["after_observed_at"]) and
                    all(key in f and f[key] == value for key, value in expected.items())):
                pending["effect"] = proof
                pending["status"] = "intermediate_effect_confirmed"
                d["operations"].append(pending)
                d["pending"] = None
                self.event("input_effect", "intermediate_effect_confirmed", proof=proof)
        if f.get("bag_blocked") is True:
            d["recovery"] = {"resume_phase": p, "kind": "bag_obstruction", "status": "needs_authored_recovery"}
            return self.wait("bag_recovery_route_unbound")
        if d["recovery"]:
            if f.get("bag_blocked") is not False:
                return self.wait("bag_recovery_effect_unconfirmed")
            d["recovery"] = None
            self.event("recovery_effect", "bag_obstruction_cleared", proof=proof)
        exact = self.goal_matches(f)
        goal = d["goal"]
        effects = {
            "identify": f.get("field_hud") is True,
            "travel_to_npc": f.get("at_npc") == goal["npc_id"],
            "accept": exact and f.get("quest_active") is True,
            "objective": exact and f.get("objective_complete") is True,
            "return_to_npc": f.get("at_npc") == goal["npc_id"] and exact and f.get("quest_active") is True,
            "report": exact and f.get("delivery_form") is True,
            "deliver": exact and f.get("reward_panel") is True,
            "reward": exact and f.get("reward_confirmed") is True,
            "completion": exact and f.get("completion_receipt") is True and f.get("reward_panel_closed") is True,
        }
        if not effects.get(p, False):
            return self.wait("effect_not_yet_observed", phase=p, observed=proof)
        pending = d["pending"]
        if pending:
            # Offline receipt or real effect must be NEWER than persisted intent.
            if observation.sequence <= pending["after_sequence"] or stamp(observation.observed_at) <= stamp(pending["after_observed_at"]):
                return self.wait("effect_predates_intent")
            pending["effect"] = proof
            pending["status"] = "effect_confirmed"
            d["operations"].append(pending)
            d["pending"] = None
        d["proofs"][p] = proof
        d["phase"] = PHASES[PHASES.index(p) + 1]
        d["status"] = ("complete" if d["mode"] == "live" else "offline_contract_passed") if d["phase"] == "done" else "running"
        d["reason"] = "effect_confirmed"
        self.event("transition", p + " -> " + d["phase"], proof=proof)
        self.save()
        return self.report()

    def dispatch(self, command, adapter, *, preconditions, source_refs, intermediate_effects=None):
        """Persist before sending; pending/unknown intents can only be reconciled."""
        d = self.data
        if d["status"] not in ("running", "waiting"):
            return self.wait("execution_not_running")
        if d["pending"]:
            return self.wait("pending_input_requires_effect_observation")
        if not preconditions or not all(value is True for value in preconditions.values()) or not source_refs:
            return self.wait("input_preconditions_unconfirmed")
        if d["recovery"]:
            return self.wait("bag_recovery_route_unbound")
        if not d["character_id"] or not d["last_observed_at"]:
            return self.wait("current_character_unavailable")
        if d["mode"] == "offline-contract" and not getattr(adapter, "offline_contract", False):
            return self.wait("offline_run_cannot_send_live_input")
        if d["mode"] == "live" and getattr(adapter, "offline_contract", False):
            return self.wait("live_run_cannot_use_fake_adapter")
        operation_id = f"{d['run_id']}:{d['phase']}:{len(d['operations'])}"
        d["pending"] = {"operation_id": operation_id, "phase": d["phase"], "status": "intent_persisted",
            "after_sequence": d["last_sequence"], "after_observed_at": d["last_observed_at"],
            "source_refs": source_refs, "preconditions": preconditions,
            "intermediate_effects": intermediate_effects}
        self.event("intent", d["phase"], operation_id=operation_id)
        self.save()
        try:
            receipt = adapter.send(command, operation_id)
        except Exception as exc:
            receipt = {"status": "unknown", "reason": type(exc).__name__ + ": " + str(exc)}
        d["pending"]["receipt"] = receipt
        d["pending"]["status"] = receipt.get("status", "unknown")
        # Even blocked input is explicit; caller must start a new intent only after
        # a separate definite-not-sent reconciliation (not implemented implicitly).
        self.save()
        return self.wait("input_awaits_effect", receipt=receipt)

    def close(self):
        self.journal.close()
