///|
/// The import pipeline keeps parsing, policy validation, and rendering as
/// explicit stages so callers can inspect partial failures.
pub enum PipelineStage {
  ReadInput
  ParseDocument
  ValidateSchema
  ValidateAccessibility
  ValidateContent
  BuildExports
} derive(Debug, Eq)

///|
pub fn PipelineStage::label(self : PipelineStage) -> String {
  match self {
    ReadInput => "read input"
    ParseDocument => "parse document"
    ValidateSchema => "validate schema"
    ValidateAccessibility => "validate accessibility"
    ValidateContent => "validate content"
    BuildExports => "build exports"
  }
}

///|
pub fn PipelineStage::ordinal(self : PipelineStage) -> Int {
  match self {
    ReadInput => 1
    ParseDocument => 2
    ValidateSchema => 3
    ValidateAccessibility => 4
    ValidateContent => 5
    BuildExports => 6
  }
}

///|
pub struct PipelineEvent {
  stage : PipelineStage
  passed : Bool
  message : String
} derive(Debug, Eq)

///|
pub fn PipelineEvent::new(
  stage : PipelineStage,
  passed : Bool,
  message : String,
) -> PipelineEvent {
  { stage, passed, message }
}

///|
pub fn PipelineEvent::status(self : PipelineEvent) -> String {
  if self.passed {
    "pass"
  } else {
    "fail"
  }
}

///|
pub struct PipelineResult {
  value : BrandBook?
  events : Array[PipelineEvent]
  diagnostics : DiagnosticBag
  exports : RenderBundle?
} derive(Debug, Eq)

///|
pub fn PipelineResult::succeeded(self : PipelineResult) -> Bool {
  self.value is Some(_) &&
  !self.diagnostics.has_errors() &&
  self.events.all(fn(event) { event.passed })
}

///|
pub fn PipelineResult::event_count(self : PipelineResult) -> Int {
  self.events.length()
}

///|
pub fn PipelineResult::failed_events(
  self : PipelineResult,
) -> Array[PipelineEvent] {
  self.events.filter(fn(event) { !event.passed })
}

///|
pub fn PipelineResult::event(
  self : PipelineResult,
  stage : PipelineStage,
) -> PipelineEvent? {
  for event in self.events {
    if event.stage == stage {
      return Some(event)
    }
  }
  None
}

///|
pub fn PipelineResult::summary(self : PipelineResult) -> String {
  let passed = self.events.filter(fn(event) { event.passed }).length()
  passed.to_string() +
  "/" +
  self.events.length().to_string() +
  " stages passed; " +
  self.diagnostics.summary()
}

///|
pub fn PipelineResult::to_markdown(self : PipelineResult) -> String {
  let status = if self.succeeded() { "success" } else { "failed" }
  let lines : Array[String] = [
    "# Import pipeline",
    "",
    "Status: **" + status + "**",
    "",
    "| Stage | Status | Message |",
    "| --- | --- | --- |",
  ]
  for event in self.events {
    lines.push(
      "| " +
      event.stage.label() +
      " | " +
      event.status() +
      " | " +
      event.message +
      " |",
    )
  }
  lines.push("")
  lines.push("Diagnostics: " + self.diagnostics.summary())
  lines.join("\n")
}

///|
pub fn PipelineResult::to_json(self : PipelineResult) -> String {
  "{\"success\":" +
  self.succeeded().to_string() +
  ",\"summary\":\"" +
  json_escape(self.summary()) +
  "\",\"events\":[" +
  self.events
  .map(fn(event) {
    "{\"stage\":\"" +
    event.stage.label() +
    "\",\"passed\":" +
    event.passed.to_string() +
    ",\"message\":\"" +
    json_escape(event.message) +
    "\"}"
  })
  .join(",") +
  "]}"
}

///|
pub fn import_brand_book(
  text : String,
  format : DocumentFormat,
) -> PipelineResult {
  let events : Array[PipelineEvent] = []
  let diagnostics = DiagnosticBag::empty()
  if text.trim().to_owned() == "" {
    events.push(PipelineEvent::new(ReadInput, false, "input is empty"))
    return {
      value: None,
      events,
      diagnostics: diagnostics.push(
        Diagnostic::error("$", "document is empty", None),
      ),
      exports: None,
    }
  }
  events.push(
    PipelineEvent::new(ReadInput, true, text.length().to_string() + " bytes"),
  )
  let parsed = parse_brand_document_as(text, format)
  let parse_passed = !parsed.diagnostics.has_errors()
  events.push(
    PipelineEvent::new(
      ParseDocument,
      parse_passed,
      parsed.diagnostics.summary(),
    ),
  )
  if !parse_passed {
    return {
      value: None,
      events,
      diagnostics: parsed.diagnostics,
      exports: None,
    }
  }
  let book = parsed.value.unwrap()
  let schema = book.validate_with_policy(ValidationPolicy::strict())
  events.push(
    PipelineEvent::new(ValidateSchema, !schema.has_errors(), schema.summary()),
  )
  let accessibility = book.accessibility_score()
  events.push(
    PipelineEvent::new(
      ValidateAccessibility,
      accessibility >= 70,
      accessibility.to_string() + "/100",
    ),
  )
  let content = book.audit_content(ContentPolicy::default())
  events.push(
    PipelineEvent::new(
      ValidateContent,
      !content.has_errors(),
      content.summary(),
    ),
  )
  let exports = book.render_bundle()
  events.push(
    PipelineEvent::new(
      BuildExports,
      exports.non_empty().length() == exports.len(),
      exports.total_bytes().to_string() + " bytes",
    ),
  )
  let mut merged = parsed.diagnostics
  for finding in schema.findings {
    merged = merged.push(
      Diagnostic::warning(finding.path, finding.message, None),
    )
  }
  { value: Some(book), events, diagnostics: merged, exports: Some(exports) }
}

///|
pub fn import_brand_book_auto(text : String) -> PipelineResult {
  import_brand_book(text, DocumentFormat::auto())
}

///|
pub fn validate_pipeline(book : BrandBook) -> PipelineResult {
  let events : Array[PipelineEvent] = []
  let schema = book.validate_with_policy(ValidationPolicy::strict())
  events.push(
    PipelineEvent::new(ValidateSchema, !schema.has_errors(), schema.summary()),
  )
  let accessibility = book.accessibility_score()
  events.push(
    PipelineEvent::new(
      ValidateAccessibility,
      accessibility >= 70,
      accessibility.to_string() + "/100",
    ),
  )
  let content = book.audit_content(ContentPolicy::default())
  events.push(
    PipelineEvent::new(
      ValidateContent,
      !content.has_errors(),
      content.summary(),
    ),
  )
  let exports = book.render_bundle()
  events.push(
    PipelineEvent::new(
      BuildExports,
      exports.total_bytes() > 0,
      exports.total_bytes().to_string() + " bytes",
    ),
  )
  let diagnostics = AuditReport::from_findings(schema.findings)
  {
    value: Some(book),
    events,
    diagnostics: diagnostics_to_bag(diagnostics),
    exports: Some(exports),
  }
}

///|
pub fn BrandBook::pipeline(self : BrandBook) -> PipelineResult {
  validate_pipeline(self)
}

///|
fn diagnostics_to_bag(report : AuditReport) -> DiagnosticBag {
  let mut bag = DiagnosticBag::empty()
  for finding in report.findings {
    bag = bag.push(Diagnostic::warning(finding.path, finding.message, None))
  }
  bag
}