// Server-Sent Events (← FastAPI's `EventSourceResponse` / `StreamingResponse`
// with `media_type="text/event-stream"`). One `ServerSentEvent` frames per the
// WHATWG event-stream format; `sse_response` concatenates a batch into a ready
// `text/event-stream` body. The framing is the value here — a server (mooncat)
// pushes frames over time; this builds each one and the response envelope.

///|
/// One Server-Sent Event. `data` is the payload (multi-line data is split into
/// several `data:` lines per the spec); `event` names the type an
/// `EventSource.addEventListener` binds to; `id` sets `lastEventId` for
/// reconnection; `retry` is the client's reconnection delay in milliseconds;
/// `comment` emits `:`-prefixed lines (a keep-alive ping carries only this).
pub(all) struct ServerSentEvent {
  data : String
  event : String?
  id : String?
  retry : Int?
  comment : String?
}

///|
/// A data-only event — the common case (`data: ...\n\n`).
pub fn ServerSentEvent::data(data : String) -> ServerSentEvent {
  { data, event: None, id: None, retry: None, comment: None, }
}

///|
/// A fully-specified event. Any field left `None` is omitted from the frame.
pub fn ServerSentEvent::new(
  data : String,
  event? : String? = None,
  id? : String? = None,
  retry? : Int? = None,
  comment? : String? = None,
) -> ServerSentEvent {
  { data, event, id, retry, comment, }
}

///|
/// A comment-only keep-alive frame (`: \n\n`) — no event is dispatched,
/// but the bytes keep the connection warm through proxies.
pub fn ServerSentEvent::keep_alive(comment? : String = "") -> ServerSentEvent {
  { data: "", event: None, id: None, retry: None, comment: Some(comment), }
}

///|
/// Split `s` on `\n` into its lines (core has no `String::split`). A trailing
/// newline yields a trailing empty line, matching how each is re-emitted as its
/// own `data:` field.
fn split_lines(s : String) -> Array[String] {
  let out : Array[String] = []
  let sb = StringBuilder()
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == 0x0A {
      out.push(sb.to_string())
      sb.reset()
    } else if s[i].to_int() != 0x0D {
      sb.write_char(s[i].unsafe_to_char())
    }
  }
  out.push(sb.to_string())
  out
}

///|
/// Encode this event as its wire frame: optional `comment` / `id` / `event` /
/// `retry` fields, then one `data:` line per line of `data`, terminated by the
/// blank line that dispatches the event.
pub fn ServerSentEvent::encode(self : ServerSentEvent) -> String {
  let sb = StringBuilder()
  match self.comment {
    Some(c) =>
      for line in split_lines(c) {
        sb.write_string(": ")
        sb.write_string(line)
        sb.write_string("\n")
      }
    None => ()
  }
  match self.id {
    Some(id) => {
      sb.write_string("id: ")
      sb.write_string(id)
      sb.write_string("\n")
    }
    None => ()
  }
  match self.event {
    Some(ev) => {
      sb.write_string("event: ")
      sb.write_string(ev)
      sb.write_string("\n")
    }
    None => ()
  }
  match self.retry {
    Some(ms) => {
      sb.write_string("retry: ")
      sb.write_string(ms.to_string())
      sb.write_string("\n")
    }
    None => ()
  }
  if self.data != "" ||
    (
      self.comment is None &&
      self.id is None &&
      self.event is None &&
      self.retry is None
    ) {
    for line in split_lines(self.data) {
      sb.write_string("data: ")
      sb.write_string(line)
      sb.write_string("\n")
    }
  }
  sb.write_string("\n")
  sb.to_string()
}

///|
/// A `text/event-stream` response whose body is the framed `events`. Sets
/// `Cache-Control: no-cache` and `Connection: keep-alive`, the headers an SSE
/// endpoint sends so intermediaries don't buffer or close the stream.
pub fn sse_response(
  events : Array[ServerSentEvent],
  status? : Int = 200,
  headers? : Array[(String, String)] = [],
) -> @moonasgi.Response {
  let sb = StringBuilder()
  for ev in events {
    sb.write_string(ev.encode())
  }
  let hs : Array[(String, String)] = [
    ("content-type", "text/event-stream; charset=utf-8"),
    ("cache-control", "no-cache"),
    ("connection", "keep-alive"),
  ]
  for h in headers {
    hs.push(h)
  }
  @moonasgi.Response::new(status, hs, @utf8.encode(sb.to_string()))
}