///|
pub struct Task {
  name : String
  cmd : String
  deps : Array[String]
  inputs : Array[String]
  outputs : Array[String]
  phony : Bool
  desc : String?
} derive(Eq, @debug.Debug)

///|
pub fn Task::is_effectively_phony(self : Task) -> Bool {
  self.phony || self.outputs.is_empty()
}

///|
pub struct Project {
  root : String
  config_path : String
  tasks : Map[String, Task]
} derive(@debug.Debug)

///|
pub(all) enum PlanReason {
  FirstRun
  PhonyTask
  CommandChanged
  InputsChanged
  OutputMissing
  OutputsChanged
  DependencyReran(Array[String])
  UpToDate
} derive(Eq, @debug.Debug)

///|
pub struct TaskPlan {
  task : Task
  reason : PlanReason
  should_run : Bool
  command_hash : String
  input_hash : String
} derive(@debug.Debug)

///|
pub struct CacheEntry {
  command_hash : String
  input_hash : String
  output_hash : String
  duration_ms : Int
} derive(ToJson, FromJson, Eq, @debug.Debug)

///|
pub struct CacheSnapshot {
  version : Int
  tasks : Map[String, CacheEntry]
} derive(ToJson, FromJson, Eq, @debug.Debug)

///|
pub struct RunSummary {
  plans : Array[TaskPlan]
  executed : Array[String]
  skipped : Array[String]
  durations_ms : Map[String, Int]
} derive(@debug.Debug)

///|
pub struct ProjectStats {
  default_target : String
  total_tasks : Int
  root_tasks : Int
  leaf_tasks : Int
  phony_tasks : Int
  concrete_tasks : Int
  tasks_with_inputs : Int
  tasks_with_outputs : Int
  total_declared_inputs : Int
  total_declared_outputs : Int
  max_depth : Int
  duplicate_outputs : Int
  overlapping_outputs : Int
  missing_inputs : Int
  unreachable_tasks : Int
} derive(@debug.Debug)

///|
pub fn empty_cache() -> CacheSnapshot {
  { version: 1, tasks: Map([]) }
}

///|
pub fn reason_message(reason : PlanReason) -> String {
  match reason {
    FirstRun => "first run"
    PhonyTask => "phony task"
    CommandChanged => "command changed"
    InputsChanged => "inputs changed"
    OutputMissing => "output missing"
    OutputsChanged => "outputs changed"
    DependencyReran(names) => {
      let joined = join_strings(names, ", ")
      "dependency reran: \{joined}"
    }
    UpToDate => "up to date"
  }
}

///|
pub fn task_summary_line(plan : TaskPlan) -> String {
  let marker = if plan.should_run { "run " } else { "skip" }
  "[\{marker}] \{plan.task.name} - \{reason_message(plan.reason)}"
}