///|
/// A lightweight relationship graph for customer-account analysis.
pub enum EntityKind {
  CustomerNode
  AccountNode
  TransactionNode
} derive(Eq, Debug)

///|
pub(all) struct Entity {
  id : String
  kind : EntityKind
}

///|
pub(all) struct Relationship {
  from_id : String
  to_id : String
  label : String
  weight : Int
}

///|
pub(all) struct TransactionGraph {
  entities : Array[Entity]
  relationships : Array[Relationship]
}

///|
pub fn TransactionGraph::empty() -> TransactionGraph {
  { entities: [], relationships: [] }
}

///|
pub fn TransactionGraph::add_entity(
  self : TransactionGraph,
  entity : Entity,
) -> TransactionGraph {
  let entities : Array[Entity] = []
  for old in self.entities {
    if old.id != entity.id {
      entities.push(old)
    }
  }
  entities.push(entity)
  { ..self, entities, }
}

///|
pub fn TransactionGraph::add_relationship(
  self : TransactionGraph,
  relationship : Relationship,
) -> TransactionGraph {
  { ..self, relationships: push_relationship(self.relationships, relationship) }
}

///|
fn push_relationship(
  old : Array[Relationship],
  item : Relationship,
) -> Array[Relationship] {
  let result : Array[Relationship] = []
  for value in old {
    result.push(value)
  }
  result.push(item)
  result
}

///|
pub fn build_graph(transactions : Array[Transaction]) -> TransactionGraph {
  let graph = TransactionGraph::empty()
  let mut current = graph
  for tx in transactions {
    current = current.add_entity({ id: tx.customer_id, kind: CustomerNode })
    current = current.add_entity({ id: tx.account_id, kind: AccountNode })
    current = current.add_entity({ id: tx.id, kind: TransactionNode })
    current = current.add_relationship({
      from_id: tx.customer_id,
      to_id: tx.account_id,
      label: "owns",
      weight: 1,
    })
    current = current.add_relationship({
      from_id: tx.account_id,
      to_id: tx.id,
      label: "used-by",
      weight: tx.amount,
    })
  }
  current
}

///|
pub fn TransactionGraph::degree(self : TransactionGraph, id : String) -> Int {
  let mut count = 0
  for edge in self.relationships {
    if edge.from_id == id || edge.to_id == id {
      count += 1
    }
  }
  count
}

///|
pub fn TransactionGraph::neighbors(
  self : TransactionGraph,
  id : String,
) -> Array[String] {
  let result : Array[String] = []
  for edge in self.relationships {
    if edge.from_id == id {
      result.push(edge.to_id)
    }
    if edge.to_id == id {
      result.push(edge.from_id)
    }
  }
  result
}

///|
pub fn high_degree_entities(
  graph : TransactionGraph,
  threshold : Int,
) -> Array[Entity] {
  let result : Array[Entity] = []
  for entity in graph.entities {
    if graph.degree(entity.id) >= threshold {
      result.push(entity)
    }
  }
  result
}

///|
pub fn shared_accounts(transactions : Array[Transaction]) -> Array[String] {
  let result : Array[String] = []
  for tx in transactions {
    let customers : Array[String] = []
    for candidate in transactions {
      if candidate.account_id == tx.account_id &&
        !contains_string(customers, candidate.customer_id) {
        customers.push(candidate.customer_id)
      }
    }
    if customers.length() > 1 && !contains_string(result, tx.account_id) {
      result.push(tx.account_id)
    }
  }
  result
}

///|
fn contains_string(items : Array[String], wanted : String) -> Bool {
  let mut found = false
  for item in items {
    if item == wanted {
      found = true
    }
  }
  found
}