from engine.gates import held
from engine.reach import Reach
from features.parts import AgentContext, ToolInterceptor
from providers import command_effects


class RefuseHeldWrites(ToolInterceptor):
    reach = Reach.BOTH

    def intercept(self, context: AgentContext, call) -> str:
        return held(context.record, context.agent.session, context.provider.is_subagent(context.hook)) if command_effects.writes(context.hook) else ""
import time
from dataclasses import dataclass
from typing import ClassVar

from engine import bus
from engine.events.agents import AgentReported, ToolFinished
from engine.events.engine import ClockTicked, FileEdited
from engine.events.resources import AnyEvent, ResourceEvent, TodoCompleted, WorkCreated
from features import trigger
from features.nudges import Sent
from features.trigger import MINUTE
from providers.payload import HookEvent
from features.parts import WHOLE_FEATURE, AgentContext, Context, Handler
from features.work_tracking import tracker
from engine.transcript import IDLE
from features.work_tracking.next import asked, carried_on, named_rows, ready, waiting_rows
from providers import PROVIDERS
from resources.types import Work
from engine.command_runs import command_runs
from controllers.types import Agents, Todos, Works

ASKED_AGAIN_AFTER = 60


@dataclass(frozen=True)
class WorkCompleted(ResourceEvent):
    on: ClassVar[str] = "work.completed"
    todo: bool = False

    @classmethod
    def read(cls, event) -> "WorkCompleted":
        return cls(n=event.n, action=event.action, type=event.type, actor=event.actor, todo=bool(event.data.get(Work.todo)))


@dataclass(frozen=True)
class PlanCompleted(ResourceEvent):
    on: ClassVar[str] = "plan.completed"


@dataclass(frozen=True)
class WorkLogged(ResourceEvent):
    on: ClassVar[str] = "work.updated"
    section: str = ""


def working(context: Context) -> list:
    return [w for w in context.journal.get(Works).rows.standing() if not w.parked]


class HoldUntilDeclared(Handler):
    def handle(self, context: Context, event: AnyEvent) -> None:
        if event.type != "work" and (event.type, event.action) != ("agent", "created"):
            return
        bus.defer_once(f"undeclared {context.record.root} {context.record.env}", lambda: self.settle(context))

    def settle(self, context: Context) -> None:
        if working(context):
            context.release()
        else:
            context.hold("undeclared held")


class OpenWork(Handler):
    def handle(self, context: Context, event: WorkCreated) -> None:
        tracker.begin(event, context.record)
        works = context.journal.get(Works)
        n = works.load(event.n).todo
        if not n:
            return
        todo = context.journal.get(Todos).load(n)
        works.link(event.n, todo.ref)
        context.journal.get(Todos).update(todo.n, status="started", work=event.n)


class CloseWork(Handler):
    def handle(self, context: Context, event: WorkCompleted) -> None:
        tracker.end(event, context.record)
        name_parked(context)
        n = context.journal.get(Works).load(event.n).todo
        if not n:
            return
        todos = context.journal.get(Todos)
        if todos.load(n).completed:
            return
        if event.todo:
            todos.complete(int(n), how=f"work {event.n} ended")
        else:
            todos.update(int(n), status="")


def name_parked(context: Context) -> None:
    parked = [w for w in context.journal.get(Works).rows.standing() if w.parked]
    agent = context.journal.get(Agents).primary()
    state, now = context.record.state(context.feature.name), time.time()
    if not parked or not agent or now - float(state.get("parked_named", 0)) <= ASKED_AGAIN_AFTER:
        return
    context.speaking_to(agent).agent.say("parked", n=parked[0].n, title=parked[0].title, why=parked[0].parked,
                                         more=f" and {len(parked) - 1} more" if len(parked) > 1 else "")
    state.set("parked_named", now)


class NameParkedOnTodoDone(Handler):
    def handle(self, context: Context, event: TodoCompleted) -> None:
        if not any(int(w.todo) == event.n for w in context.journal.get(Works).rows.standing()):
            name_parked(context)


def name_unblocked(context: Context, ref: str) -> None:
    todos, agent = context.journal.get(Todos), context.journal.get(Agents).primary()
    if not agent:
        return
    for row in [r for r in todos.rows.standing() if ref in r.after and not r.blocked and not todos.waits(r)]:
        context.speaking_to(agent).agent.say("unblocked", n=row.n, title=row.title, closed=ref.replace(":", " "))


class UnblockWaitingRows(Handler):
    def handle(self, context: Context, event: TodoCompleted) -> None:
        if not context.journal.get(Todos).load(event.n).struck:
            name_unblocked(context, f"todo:{event.n}")


class UnblockWhenPlanFinishes(Handler):
    def handle(self, context: Context, event: PlanCompleted) -> None:
        name_unblocked(context, f"plan:{event.n}")


class AskStillBlocked(Handler):
    def handle(self, context: Context, event: TodoCompleted) -> None:
        state = context.record.state(context.feature.name)
        closed = int(state.get("closed", 0)) + 1
        state.set("closed", closed)
        agent = context.journal.get(Agents).primary()
        if not agent or closed % max(1, int(context.settings["ask_blocked_every"])):
            return
        asked_at, now = state.get("asked", {}), time.time()
        due = [r for r in context.journal.get(Todos).rows.standing() if r.blocked and now - float(asked_at.get(str(r.n), 0)) > ASKED_AGAIN_AFTER]
        for row in [r for r in due if not asked(context.record, r)]:
            context.speaking_to(agent).agent.say("still blocked", n=row.n, title=row.title, why=row.blocked.rstrip("."))
            asked_at[str(row.n)] = now
        state.set("asked", asked_at)


class EndWorkWithTodo(Handler):
    def handle(self, context: Context, event: TodoCompleted) -> None:
        works = context.journal.get(Works)
        for work in works.rows.standing():
            if int(work.todo) == event.n:
                works.complete(work.n, how=context.journal.get(Todos).load(event.n).outcome or f"todo {event.n} done")


class TrackFiles(Handler):
    def handle(self, context: AgentContext, event: FileEdited) -> None:
        for work in working(context)[:1]:
            tracker.record_edit(context.agent.row, context.record, work, event)


POLLED = 3
OPEN_REMINDERS = 3
FIRST_AFTER = 5
CARRY_ON_TIMES = 3
POLLING = "polling"
POLLING_NO_WAKE = "polling, no wake"


class NameRepeatedChecks(Handler):
    def handle(self, context: AgentContext, event: ToolFinished) -> None:
        shell = [c["command"] for c in context.agent.row.data.get("commands") or [] if c.get("tool") == "Bash" and c.get("command")][-POLLED:]
        if len(shell) < POLLED or len(set(shell)) > 1 or any(w.awaiting for w in working(context)):
            return
        if context.once("polled", shell[-1]):
            provider = PROVIDERS.get(context.agent.row.provider)
            context.agent.whisper(POLLING if provider and provider.background_wakes else POLLING_NO_WAKE, times=POLLED, command=shell[-1][:80])


class ClearWaitOnActivity(Handler):
    def handle(self, context: AgentContext, event: ToolFinished) -> None:
        if not context.agent.row.wrote:
            return
        runs = command_runs(context.agent.row)
        started = runs[-1].at if runs else 0.0
        for w in working(context)[:1]:
            if not w.is_self_clearing or started <= float(w.awaiting_since):
                continue
            context.journal.get(Works).update(w.n, awaiting="")
            context.agent.whisper("wait cleared", awaiting=w.awaiting)


class AskStillAwaiting(Handler):
    def handle(self, context: AgentContext, event: ClockTicked) -> None:
        every = context.settings.ask_awaiting_every
        for w in working(context)[:1]:
            if not w.is_self_clearing or not every:
                continue
            minutes = int((time.time() - (w.awaiting_since or time.time())) // 60)
            if minutes >= every and context.once("awaiting asked", f"{w.n}:{minutes // every}"):
                context.agent.say("still awaiting", awaiting=w.awaiting, minutes=minutes)


class RemindOpenWork(Handler):
    behaviour = WHOLE_FEATURE

    def handle(self, context: AgentContext, event: AgentReported) -> None:
        for w in [w for w in working(context)[:1] if context.at_most("open", f"{w.n}:{w.updated}", OPEN_REMINDERS)]:
            context.agent.say("open" if w.sections else "unlogged", n=w.n)


class CountEdits(Handler):
    hooks = (HookEvent.POST_TOOL_USE,)
    def handle(self, context: AgentContext, event: AgentReported) -> None:
        work = working(context)[:1]
        if not context.agent.row.wrote or not work:
            return
        row, name = context.agent.row, context.feature.name
        edits = trigger.last(context.record, row.title, name).edits + 1
        trigger.write(context.record, row, name, edits=edits)
        every = context.settings.name_work_every
        if every and edits % every == 0:
            context.agent.whisper("in hand", n=work[0].n, title=work[0].title)
        if edits >= context.settings.log_after:
            context.hold("log held", edits=edits, n=work[0].n)


class ResetEditsOnLog(Handler):
    def handle(self, context: Context, event: WorkLogged) -> None:
        if not event.section:
            return
        for row in context.feature.reached(context.record, context.feature.lines["log held"].reach):
            trigger.write(context.record, row, context.feature.name, edits=0)
        context.release()


def next_row(standing: bool):
    def about(context, agent) -> list[Sent]:
        open_work = working(context)
        if bool(open_work) != standing or (open_work and open_work[0].awaiting):
            return []
        return [Sent(f"{row.n}:{row.updated}", {"n": row.n, "work": open_work[0].n} if standing else {"n": row.n}) for row in ready(context.record)]
    return about




def stopped_with_work(context, agent) -> list[Sent]:
    every = float(context.feature.cadence(context.record, "carry on").every) * MINUTE
    carried = carried_on(context.record)[:1]
    late = agent.idle_for - FIRST_AFTER * MINUTE
    if not carried or late < 0 or late // every >= CARRY_ON_TIMES:
        return []
    work = carried[0]
    return [Sent(f"{work.n}:{work.updated}:{int(late // every)}", {"n": work.n, "title": work.title, "minutes": int(agent.idle_for // MINUTE)})]


def nothing_ready(context, agent) -> list[Sent]:
    if agent.idle_for < FIRST_AFTER * MINUTE or working(context) or ready(context.record):
        return []
    held = waiting_rows(context.record)
    return [Sent(",".join(str(t.n) for t in held), {"rows": named_rows(held)})] if held else []
from features.base import Behaviour, FeatureDetails, Line
from features.settings import Setting
from features.trigger import IDLE, MINUTES, Trigger, WORKED
from features.groups import Group


class WorkDetails(FeatureDetails):
    explains = 'The agent records the work it starts, changes, and closes. You can follow its progress from the to-do.'
    name = "work_tracking"
    group = Group.WORK_TRACKING
    trigger_label = "Check for open work"
    skill_of = "todos"
    when = "you start, log, park, await or end work, or before the first write"

    title = "Work tracking"

    speaks_while_waiting = True

    abstract = """
        The agent must open work before it changes files. Work is linked to its to-do and keeps a
        log. After twenty edits without a log entry, file changes are blocked until it writes one.
    """

    help = """
        One piece of work is in hand at a time: starting another is refused until this one is
        ended or parked. Take a row with journal todo start <n>, or start work of its own with
        journal work start "<title>"; log each decision and turn with journal work log
        "<message>" (work.log_after, 20 edits without an entry holds the writes); end it with
        journal work end <n> --how "<what landed>", and --set todo=<n> closes the row with it.

        journal work park "<why>" sets it aside with no clock — it stays open, stops being
        nudged and stops holding writes, and journal work resume <n> picks it up again. Park
        when nothing stops the work but something else goes first by choice. What cannot go
        ahead until something happens is blocked, not parked: journal todo after, todo ask or
        todo block on its row. Never park to wait for an answer you could carry on without.

        journal work await "<what>" says you are waiting for something outside your hands,
        such as a long build or a run in Docker, in your own words, and the chat shows it. Say it
        once instead of writing another line each time nothing has changed. Working again clears
        it, and you are told that it was cleared; parking clears it too. While it stands you
        are told every five minutes (work.ask_awaiting_every) to check the thing you wait
        on and carry on or wait again.

        Auto mode is on by default: it is the user's word to work the list and decide without
        blocking questions, and switching it off stops the offers. The next ready row by priority is offered on idle
        while nothing is open. Stopping with work open while another row is ready earns the
        same offer: a row that waits on the user gets its question with todo ask and the next
        row is taken, so the agent stops only when nothing ready is left. Five minutes quiet
        with unparked work open earns a direct question, are you still working? A row is
        ready when it is not blocked, waits on no open row or question, and its plan's phase
        is current.
    """

    aliases = (("auto", "auto"), "work")

    trigger = Trigger(on=WORKED)

    behaviours = [
        Behaviour(
            name="auto",
            title="Start the next to-do without asking",
            default=True,
            trigger=Trigger(on=IDLE),
        ),
        Behaviour(
            name="carry on",
            title="Tell a stopped agent to carry on",
            trigger=Trigger(every=10, unit=MINUTES),
        ),
    ]

    settings = [
        Setting(
            name="log_after",
            default=20,
            title="Block edits after this many with nothing logged",
            unit="edits",
        ),
        Setting(
            name="ask_awaiting_every",
            default=5,
            title="Ask what the agent is waiting on every",
            unit="minutes",
        ),
        Setting(
            name="ask_blocked_every",
            default=1,
            title="Recheck blocked to-dos after every",
            unit="closed to-dos",
        ),
        Setting(
            name="name_work_every",
            default=10,
            title="Remind the agent of its current work every",
            unit="edits",
        ),
    ]

    lines = [
        Line(
            name="parked",
            title="work {{n}}, {{title}}, is still parked{{more}} - can you continue it now?",
            brief="it was parked because: {{why}}. journal work resume {{n}} picks it up again.",
        ),
        Line(
            name="unblocked",
            title="todo {{n}}, {{title}}, is unblocked - {{closed}} closed",
            brief="journal todo start {{n}} when it is next",
        ),
        Line(
            name="still blocked",
            reply_kept=True,
            title="todo {{n}}, {{title}}, is still blocked - is it still?",
            brief="""
                it is blocked because: {{why}}. If it is not any more, journal todo unblock {{n}}. If it waits
                on a person or a decision, make it a question to them: journal todo ask {{n}} "<who decides
                what>", and the row waits on their answer. Otherwise tell the user in the chat what it waits
                on, in their terms, and propose how to clear it.
            """,
        ),
        Line(
            name="open",
            title="work {{n}} is still open",
            brief='end it or park it before you stop: journal work end {{n}} --how "<what landed>", or journal work park {{n}} "<why it waits>"',
            while_waiting=False,
        ),
        Line(
            name="unlogged",
            title="work {{n}} is still open, with nothing logged",
            brief="""
                journal work log {{n}} "<what was decided or done, and why>" — then journal work
                end {{n}} --how "<what landed>", or journal work park {{n}} "<why it waits>"
            """,
            while_waiting=False,
        ),
        Line(
            name="in hand",
            title="work {{n}} in hand — {{title}}",
            brief="if this is not what you are doing, end it or park it and start the work you are in",
        ),
        Line(
            name="log held",
            title='{{edits}} edits since work {{n}} was last logged: journal work log "<what was decided or done, and why>" before any other write',
        ),
        Line(
            name="undeclared held",
            title='nothing is open, so this write would not be filed: journal work start "<the work>" first',
        ),
        Line(
            name="next",
            title="todo {{n}} next",
        ),
        Line(
            name="next while waiting",
            title="auto mode is on and work {{work}} stands still while todo {{n}} is ready",
            brief="""
                if work {{work}} waits on the user, decide it yourself when you can; otherwise put the question on
                its row with journal todo ask, end or park the work, and start todo {{n}}. Stop only when nothing
                ready is left.
            """,
        ),
        Line(
            name="nothing ready",
            reply_kept=True,
            title="nothing is ready: every open row waits",
            brief="""
                {{rows}}. For each that waits on a person or a decision, put it to them now with journal todo ask <n>
                "<who decides what>"; unblock any that can go on and work it. Stop only when each one waits on a question.
            """,
        ),
        Line(
            name="carry on",
            reply_kept=True,
            title="you stopped {{minutes}} minutes ago with work {{n}}, {{title}}, in hand",
            brief="""
                carry on with it now. If it waits on something outside your hands, say journal work await
                "<what you wait for>"; if it waits on the user, put the question on its row with journal todo ask
                and take the next ready row; if something else goes first, journal work park {{n}} "<why>".
            """,
        ),
        Line(
            name="polling",
            title="you ran the same check {{times}} times in a row - {{command}}",
            brief="""
                if you are waiting for something to change, say journal work await "<what you wait for>" and end
                your turn: you are asked to look again every five minutes, and a background command tells you
                itself when it ends. Keep checking only if each look moves the work on.
            """,
        ),
        Line(
            name="polling, no wake",
            title="you ran the same check {{times}} times in a row - {{command}}",
            brief="""
                if you are waiting for something to change, say journal work await "<what you wait for>" and end
                your turn: the journal types to you every five minutes to look again, and tells you when a
                background command you left running ends.
            """,
        ),
        Line(
            name="wait cleared",
            title="your wait for {{awaiting}} is over, because you are working again",
            brief='say journal work await "<what you wait for>" again if you are still only waiting',
        ),
        Line(
            name="still awaiting",
            reply_kept=True,
            title="check {{awaiting}} now - you have waited {{minutes}} min",
            brief="""
                look at the thing itself: the background shell's output, the process, the run's
                status. If it is still going, say journal work await "<what you wait for>" again and
                wait. If it finished or failed, journal work log what came of it and take the next
                step. Never wait for something you can do without.
            """,
        ),
    ]
