///|
/// Dependency relationships inferred from signal timing.
pub enum DependencyKind {
  DataFollowsControl
  ControlPrecedesData
  SameTimestamp
  ClockDriven
  CorrelatedActivity
} derive(Debug, Eq)

///|
/// One weighted dependency hypothesis.
pub struct SignalDependency {
  source_id : String
  target_id : String
  kind : DependencyKind
  evidence_count : Int
  average_lag : Double
  confidence : Double
  first_time : Int64?
  last_time : Int64?
} derive(Debug)

///|
/// Dependency report for a trace.
pub struct DependencyReport {
  dependencies : Array[SignalDependency]
  signal_count : Int
  observation_count : Int
  threshold : Int64
} derive(Debug)

///|
/// List dependency categories.
pub fn dependency_kind_catalog() -> Array[DependencyKind] {
  [
    DataFollowsControl,
    ControlPrecedesData,
    SameTimestamp,
    ClockDriven,
    CorrelatedActivity,
  ]
}

///|
/// Infer dependencies between declared signals using nearby changes.
pub fn infer_signal_dependencies(
  trace : Trace,
  threshold : Int64,
) -> DependencyReport {
  let limit = Int64::max(0L, threshold)
  let dependencies : Array[SignalDependency] = []
  let observations = trace.changes.length()
  for target in trace.signals {
    for source in trace.signals {
      if source.id == target.id {
        continue
      }
      let pairs = dependency_pairs(trace, source.id, target.id, limit)
      if pairs.length() == 0 {
        continue
      }
      let total = pairs.fold(init=0L, fn(sum, item) { sum + item.1 })
      let average = total.to_double() / pairs.length().to_double()
      let confidence = Int::min(
          100,
          pairs.length() * 100 / Int::max(1, trace.changes.length()),
        ).to_double() /
        100.0
      dependencies.push({
        source_id: source.id,
        target_id: target.id,
        kind: if pairs.any(fn(item) { item.1 == 0L }) {
          SameTimestamp
        } else {
          DataFollowsControl
        },
        evidence_count: pairs.length(),
        average_lag: average,
        confidence,
        first_time: Some(pairs[0].0),
        last_time: Some(pairs[pairs.length() - 1].0),
      })
    }
  }
  {
    dependencies,
    signal_count: trace.signals.length(),
    observation_count: observations,
    threshold: limit,
  }
}

///|
/// Return high-confidence dependencies only.
pub fn DependencyReport::strong(
  self : DependencyReport,
  minimum : Double,
) -> Array[SignalDependency] {
  self.dependencies.filter(fn(item) { item.confidence >= minimum })
}

///|
/// Return dependencies originating from one signal.
pub fn DependencyReport::from(
  self : DependencyReport,
  source_id : String,
) -> Array[SignalDependency] {
  self.dependencies.filter(fn(item) { item.source_id == source_id })
}

///|
/// Return dependencies targeting one signal.
pub fn DependencyReport::to(
  self : DependencyReport,
  target_id : String,
) -> Array[SignalDependency] {
  self.dependencies.filter(fn(item) { item.target_id == target_id })
}

///|
/// Build an adjacency map for scheduling and visualization.
pub fn dependency_adjacency(
  report : DependencyReport,
) -> Array[(String, Array[String])] {
  let result : Array[(String, Array[String])] = []
  for item in report.dependencies {
    let mut found = false
    for index, group in result {
      if group.0 == item.source_id {
        if !group.1.contains(item.target_id) {
          group.1.push(item.target_id)
        }
        result[index] = group
        found = true
        break
      }
    }
    if !found {
      result.push((item.source_id, [item.target_id]))
    }
  }
  result
}

///|
/// Export dependencies as CSV.
pub fn dependencies_csv(items : Array[SignalDependency]) -> String {
  let out = StringBuilder()
  out.write_string(
    "source,target,kind,evidence,average_lag,confidence,first,last\n",
  )
  for item in items {
    out.write_string(
      escape_text(item.source_id) +
      "," +
      escape_text(item.target_id) +
      "," +
      dependency_kind_name(item.kind) +
      "," +
      item.evidence_count.to_string() +
      "," +
      item.average_lag.to_string() +
      "," +
      item.confidence.to_string() +
      "," +
      dependency_optional_time(item.first_time) +
      "," +
      dependency_optional_time(item.last_time) +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Render a dependency report summary.
pub fn DependencyReport::to_text(self : DependencyReport) -> String {
  "signals=" +
  self.signal_count.to_string() +
  " observations=" +
  self.observation_count.to_string() +
  " dependencies=" +
  self.dependencies.length().to_string() +
  " threshold=" +
  self.threshold.to_string()
}

///|
/// Render a dependency report as JSON.
pub fn DependencyReport::to_json(self : DependencyReport) -> String {
  let items = self.dependencies.map(dependency_json).join(",")
  "{\"signals\":" +
  self.signal_count.to_string() +
  ",\"observations\":" +
  self.observation_count.to_string() +
  ",\"threshold\":" +
  self.threshold.to_string() +
  ",\"dependencies\":[" +
  items +
  "]}"
}

///|
/// Find target changes that follow a source change within a lag threshold.
fn dependency_pairs(
  trace : Trace,
  source_id : String,
  target_id : String,
  threshold : Int64,
) -> Array[(Int64, Int64)] {
  let result : Array[(Int64, Int64)] = []
  let source_times = trace.changes
    .filter(fn(item) { item.id == source_id })
    .map(fn(item) { item.time })
  let target_times = trace.changes
    .filter(fn(item) { item.id == target_id })
    .map(fn(item) { item.time })
  for target_time in target_times {
    let mut closest : Int64? = None
    for source_time in source_times {
      if source_time <= target_time {
        let lag = target_time - source_time
        if lag <= threshold {
          closest = match closest {
            Some(value) => Some(Int64::min(value, lag))
            None => Some(lag)
          }
        }
      }
    }
    match closest {
      Some(lag) => result.push((target_time, lag))
      None => ()
    }
  }
  result
}

///|
/// Name a dependency category.
pub fn dependency_kind_name(kind : DependencyKind) -> String {
  match kind {
    DataFollowsControl => "data-follows-control"
    ControlPrecedesData => "control-precedes-data"
    SameTimestamp => "same-timestamp"
    ClockDriven => "clock-driven"
    CorrelatedActivity => "correlated-activity"
  }
}

///|
/// Encode an optional dependency timestamp.
fn dependency_optional_time(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => ""
  }
}

///|
/// Encode one dependency as JSON.
fn dependency_json(item : SignalDependency) -> String {
  "{\"source\":\"" +
  escape_text(item.source_id) +
  "\",\"target\":\"" +
  escape_text(item.target_id) +
  "\",\"kind\":\"" +
  dependency_kind_name(item.kind) +
  "\",\"evidence\":" +
  item.evidence_count.to_string() +
  ",\"average_lag\":" +
  item.average_lag.to_string() +
  ",\"confidence\":" +
  item.confidence.to_string() +
  "}"
}