"""What a workflow run is, and what state it is in.

A workflow must be resumable: the process will be restarted mid-run, a step will
time out, the CMS will be briefly unreachable. None of those may cost the work
already done, and none may cause it to be done twice.

That is only achievable if progress is recorded *per step* and persisted as it
happens. A run that records only "started" and "finished" has to begin again
from the top after any interruption — which, for a workflow that files documents
and raises invoices, means doing real things a second time.
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any


class RunState(StrEnum):
    PENDING = "pending"
    RUNNING = "running"

    AWAITING_APPROVAL = "awaiting_approval"
    """Stopped at a step a human must approve (ADR-0004). Not a failure —
    the expected outcome for any step that writes to client data."""

    COMPLETED = "completed"
    FAILED = "failed"

    @property
    def is_terminal(self) -> bool:
        return self in {RunState.COMPLETED, RunState.FAILED}

    @property
    def is_resumable(self) -> bool:
        """Awaiting approval is resumable — that is the whole point of pausing."""
        return self in {RunState.PENDING, RunState.RUNNING, RunState.AWAITING_APPROVAL}


class StepState(StrEnum):
    PENDING = "pending"
    SUCCEEDED = "succeeded"
    FAILED = "failed"
    AWAITING_APPROVAL = "awaiting_approval"
    SKIPPED = "skipped"


@dataclass(slots=True)
class StepRecord:
    """One step's progress within a run."""

    name: str
    tool: str
    state: StepState = StepState.PENDING
    attempts: int = 0
    output: dict[str, Any] = field(default_factory=dict)
    error: str | None = None
    confidence: float | None = None
    duration_ms: int | None = None
    started_at: datetime | None = None
    finished_at: datetime | None = None

    proposed: dict[str, Any] = field(default_factory=dict)
    """What an approval-gated step asked a human to authorise. Kept after the
    step runs: the audit question is what was approved, not what was sent."""

    approved: bool = False
    """A human has authorised this step. Held on the step rather than spent on
    one call, so retrying a transient failure does not ask them twice."""

    approved_by: str | None = None
    amendments: dict[str, Any] = field(default_factory=dict)
    """What the human changed before approving — most often the client a
    document was about to be filed against. Recorded separately from
    ``proposed`` so the correction itself is visible in the audit trail."""

    @property
    def is_done(self) -> bool:
        """Succeeded or deliberately skipped — either way, do not run it again.

        This is the check that makes a retry safe: a resumed run must never
        re-execute a step that already did its work.
        """
        return self.state in {StepState.SUCCEEDED, StepState.SKIPPED}


@dataclass(slots=True)
class WorkflowRun:
    """One execution of a workflow definition.

    ``context`` accumulates each step's output, so a later step can use what an
    earlier one produced. It is persisted with the run, which is what allows a
    resume to continue with everything the first attempt had learned.
    """

    workflow: str
    context: dict[str, Any] = field(default_factory=dict)
    steps: list[StepRecord] = field(default_factory=list)
    state: RunState = RunState.PENDING
    error: str | None = None

    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
    updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))

    version: int = 0
    """How many times this run has been stored.

    Zero means it has never been written. Every save checks the version it read
    against what is on disk and refuses if they differ — which is what stops two
    processes that both loaded the same run from overwriting each other's
    progress, one of them silently losing a completed step.

    Enforced by every store, including the in-process one. A concurrency rule
    that only the production store applied would be a rule no test could reach.
    """

    archived_at: datetime | None = None
    """Set when a finished run is moved out of the working set. The run is kept:
    it is the account of what happened, and the reason to stop loading it is
    volume, not irrelevance."""

    def step(self, name: str) -> StepRecord | None:
        return next((s for s in self.steps if s.name == name), None)

    @property
    def next_step(self) -> StepRecord | None:
        """The first step still to do."""
        return next((s for s in self.steps if not s.is_done), None)

    @property
    def completed_steps(self) -> list[StepRecord]:
        return [s for s in self.steps if s.is_done]

    @property
    def progress(self) -> str:
        return f"{len(self.completed_steps)}/{len(self.steps)}"

    def touch(self) -> None:
        self.updated_at = datetime.now(UTC)
