///|
/// Event type for provenance trace.
pub(all) enum TraceEventType {
  Planned
  Started
  Completed
  FailedEvent
  SkippedEvent
  Note
} derive(Eq, Debug)

///|
/// A single trace event. Timestamp is a string so users can choose wall-clock,
/// logical, or deterministic timestamps.
pub struct TraceEvent {
  priv task_id : TaskId
  priv event_type : TraceEventType
  priv message : String
  priv timestamp : String
} derive(Eq, Debug)

///|
/// Mutable trace log.
pub struct Trace {
  priv mut events : Array[TraceEvent]
} derive(Debug)

///|
/// Build an empty trace log.
pub fn Trace::new() -> Trace {
  { events: [] }
}

///|
/// Build a trace event.
pub fn TraceEvent::new(
  task_id : TaskId,
  event_type : TraceEventType,
  message : String,
  timestamp : String,
) -> TraceEvent {
  { task_id, event_type, message, timestamp }
}

///|
/// Return the task id associated with this event.
pub fn TraceEvent::task_id(self : TraceEvent) -> TaskId {
  self.task_id
}

///|
/// Return this event's type.
pub fn TraceEvent::event_type(self : TraceEvent) -> TraceEventType {
  self.event_type
}

///|
/// Return this event's human-readable message.
pub fn TraceEvent::message(self : TraceEvent) -> String {
  self.message
}

///|
/// Return this event's timestamp string.
pub fn TraceEvent::timestamp(self : TraceEvent) -> String {
  self.timestamp
}

///|
/// Append an event.
pub fn Trace::record(self : Trace, event : TraceEvent) -> Unit {
  self.events.push(event)
}

///|
/// Return a detached copy of events in append order.
pub fn Trace::events(self : Trace) -> Array[TraceEvent] {
  self.events_snapshot()
}

///|
/// Return a detached copy of events in append order.
pub fn Trace::events_snapshot(self : Trace) -> Array[TraceEvent] {
  self.events.copy()
}

///|
/// Return a detached copy of this trace log.
pub fn Trace::snapshot(self : Trace) -> Trace {
  { events: self.events.copy() }
}

///|
/// Return the number of recorded events.
pub fn Trace::event_count(self : Trace) -> Int {
  self.events.length()
}

///|
/// Return events for a single task id in append order.
pub fn Trace::events_for(self : Trace, task_id : TaskId) -> Array[TraceEvent] {
  let out : Array[TraceEvent] = []
  for event in self.events {
    if event.task_id == task_id {
      out.push(event)
    }
  }
  out
}

///|
/// Return the most recently appended event for a task.
pub fn Trace::latest_for(self : Trace, task_id : TaskId) -> TraceEvent? {
  let mut index = self.events.length()
  while index > 0 {
    index = index - 1
    if self.events[index].task_id == task_id {
      return Some(self.events[index])
    }
  }
  None
}

///|
/// Render a compact Markdown summary of trace events.
pub fn Trace::summary_markdown(self : Trace) -> String {
  let out = StringBuilder()
  out <+ "## Trace Summary\n\n"
  if self.events.is_empty() {
    out <+ "- No trace events recorded.\n"
    return out.to_string()
  }
  for event in self.events {
    out <+ "- `\{event.timestamp()}` \{event.task_id().value()} "
    out <+ "\{event.event_type().label()}: \{event.message()}\n"
  }
  out.to_string()
}

///|
/// Return a stable human-readable task status label.
pub fn TaskStatus::label(self : TaskStatus) -> String {
  match self {
    Pending => "pending"
    Ready => "ready"
    Running => "running"
    Succeeded => "succeeded"
    Failed(reason) => "failed: \{reason}"
    Skipped(reason) => "skipped: \{reason}"
  }
}

///|
/// Return a stable machine-readable status kind without an error reason.
pub fn TaskStatus::kind(self : TaskStatus) -> String {
  match self {
    Pending => "pending"
    Ready => "ready"
    Running => "running"
    Succeeded => "succeeded"
    Failed(_) => "failed"
    Skipped(_) => "skipped"
  }
}

///|
/// Return the reason attached to a failed or skipped status.
pub fn TaskStatus::reason(self : TaskStatus) -> String? {
  match self {
    Failed(reason) | Skipped(reason) => Some(reason)
    _ => None
  }
}

///|
/// Return a stable human-readable event type label.
pub fn TraceEventType::label(self : TraceEventType) -> String {
  match self {
    Planned => "planned"
    Started => "started"
    Completed => "completed"
    FailedEvent => "failed"
    SkippedEvent => "skipped"
    Note => "note"
  }
}

///|
/// Return a readable diagnostic message for a graph error.
pub fn GraphError::message(self : GraphError) -> String {
  match self {
    DuplicateTask(id) => "duplicate task: \{id.value()}"
    DuplicateDependency(dep) =>
      "duplicate dependency: \{dep.before().value()} -> \{dep.after().value()}"
    MissingTask(id) => "missing task: \{id.value()}"
    MissingDependencyEndpoint(dep) =>
      "dependency endpoint missing: \{dep.before().value()} -> \{dep.after().value()}"
    CycleDetected(path) => {
      let rendered = format_id_list(path, " -> ")
      "cycle detected: \{rendered}"
    }
  }
}

///|
/// Return a readable diagnostic for a checked status transition.
pub fn StatusTransitionError::message(self : StatusTransitionError) -> String {
  match self {
    TransitionMissingTask(id) =>
      "missing task for status transition: \{id.value()}"
    InvalidStatusTransition(id, before, after) =>
      "invalid status transition for \{id.value()}: \{before.label()} -> \{after.label()}"
  }
}

///|
/// Return a readable diagnostic for snapshot validation.
pub fn SnapshotError::message(self : SnapshotError) -> String {
  match self {
    SnapshotGraphError(err) => err.message()
    StaleExecutionPlan => "execution plan does not match the current graph"
    UnknownTraceTask(id) => "trace references unknown task: \{id.value()}"
  }
}

///|
fn format_id_list(ids : Array[TaskId], sep : String) -> String {
  let out = StringBuilder()
  for i = 0; i < ids.length(); i = i + 1 {
    if i > 0 {
      out.write_string(sep)
    }
    out.write_string(ids[i].value())
  }
  out.to_string()
}