///|
/// A validated deployable set of patterns, budgets and optional alert projections.
pub struct Program {
  patterns : Array[Pattern]
  limits : Limits
  projections : Array[ProjectionPlan]
}

///|
pub fn Program::new(
  patterns : Array[Pattern],
  limits~ : Limits = Limits::default(),
  projections~ : Array[ProjectionPlan] = [],
) -> Program raise CepError {
  let validated = Engine::new(patterns, limits~)
  if projections.length() > patterns.length() {
    raise Invalid(
      path="program.projections",
      reason="at most one projection per pattern",
    )
  }
  let seen : Map[String, Bool] = Map([])
  let plans = []
  for plan in projections {
    if seen.contains(plan.pattern) {
      raise Invalid(
        path="program.projections",
        reason="duplicate projection for \{plan.pattern}",
      )
    }
    seen.set(plan.pattern, true)
    let mut target : Pattern? = None
    for pattern in patterns {
      if pattern.id == plan.pattern {
        target = Some(pattern)
      }
    }
    match target {
      Some(pattern) => plans.push(ProjectionPlan::new(pattern, plan.columns))
      None =>
        raise Invalid(
          path="program.projections",
          reason="projection references unknown pattern",
        )
    }
  }
  { patterns: validated.patterns, limits, projections: plans, }
}

///|
pub fn Program::patterns(self : Program) -> Array[Pattern] {
  self.patterns.map(clone_pattern)
}

///|
pub fn Program::limits(self : Program) -> Limits {
  self.limits
}

///|
pub fn Program::projections(self : Program) -> Array[ProjectionPlan] {
  self.projections.map(plan => { ..plan, columns: plan.columns.copy(), })
}

///|
pub fn Program::engine(self : Program) -> Engine raise CepError {
  Engine::new(self.patterns, limits=self.limits)
}

///|
pub fn Program::monitor(
  self : Program,
  max_commands~ : Int32 = 10000,
) -> Monitor raise CepError {
  Monitor::new(self.patterns, limits=self.limits, max_commands~)
}

///|
pub(all) struct AlertRecord {
  pattern : String
  key : String
  identity : String
  fields : Array[Field]
}

///|
/// Project only matches with a configured projection. Failure returns no partial
/// records and never changes the already committed recognition batch.
pub fn Program::project(
  self : Program,
  batch : Batch,
) -> Array[AlertRecord] raise CepError {
  let records = []
  for value in batch.matches {
    for plan in self.projections {
      if plan.pattern == value.pattern {
        records.push({
          pattern: value.pattern,
          key: value.key,
          identity: value.identity(),
          fields: plan.apply(value),
        })
      }
    }
  }
  records
}

///|
pub fn alert_record_to_json(record : AlertRecord) -> Json raise CepError {
  {
    "pattern": Json::string(record.pattern),
    "key": Json::string(record.key),
    "identity": Json::string(record.identity),
    "fields": projected_record_to_json(record.fields),
  }
}