///|
/// Transport-neutral HTTP request parsed from a server or test fixture.
pub(all) struct HttpRequest {
  verb : String
  path : String
  version : String
  headers : Map[String, String]
  body : String
} derive(Eq, Debug)

///|
pub(all) enum HttpParseResult {
  Parsed(HttpRequest)
  InvalidHttp(String)
} derive(Eq, Debug)

///|
/// HTTP response returned by a WebHook endpoint.
pub(all) struct HttpResponse {
  status : Int
  reason : String
  headers : Map[String, String]
  body : String
} derive(Eq, Debug)

///|
/// Stateful endpoint joining HTTP parsing, HMAC verification, JSON parsing,
/// middleware, deduplication, and routing.
pub struct WebhookEndpoint {
  path : String
  adapter : ProviderAdapter
  secret : String
  router : HookRouter
  middlewares : Array[HookMiddleware]
  deduper : InMemoryDeduper
  max_body_bytes : Int
}

///|
pub fn HttpResponse::HttpResponse(
  status : Int,
  reason : StringView,
  body : StringView,
) -> HttpResponse {
  let owned_body = body.to_owned()
  {
    status,
    reason: reason.to_owned(),
    headers: Map([
      ("content-type", "text/plain; charset=utf-8"),
      ("content-length", @utf8.encode(owned_body).length().to_string()),
    ]),
    body: owned_body,
  }
}

///|
/// Serialize a response for a TCP/HTTP server binding.
pub fn HttpResponse::render(self : HttpResponse) -> String {
  let builder = StringBuilder::new()
  builder.write_string("HTTP/1.1 ")
  builder.write_string(self.status.to_string())
  builder.write_string(" ")
  builder.write_string(self.reason)
  builder.write_string("\r\n")
  for name, value in self.headers {
    builder.write_string(name)
    builder.write_string(": ")
    builder.write_string(value)
    builder.write_string("\r\n")
  }
  builder.write_string("\r\n")
  builder.write_string(self.body)
  builder.to_string()
}

///|
/// Parse an HTTP/1.x request and normalize header names to lowercase.
pub fn parse_http_request(raw : String) -> HttpParseResult {
  let boundary = match raw.find("\r\n\r\n") {
    Some(offset) => offset
    None => return InvalidHttp("missing HTTP header terminator")
  }
  let head = raw.view(start_offset=0, end_offset=boundary).to_owned()
  let body = raw.view(start_offset=boundary + 4).to_owned()
  let lines = head.split("\r\n").to_array()
  if lines.length() == 0 {
    return InvalidHttp("missing request line")
  }
  let request_line = lines[0].to_owned().split(" ").to_array()
  if request_line.length() != 3 {
    return InvalidHttp("malformed request line")
  }
  let verb = request_line[0].to_owned()
  let path = request_line[1].to_owned()
  let version = request_line[2].to_owned()
  if !version.has_prefix("HTTP/1.") {
    return InvalidHttp("unsupported HTTP version")
  }
  let headers : Map[String, String] = Map([])
  for index in 1.. offset
      None => return InvalidHttp("malformed HTTP header")
    }
    if colon == 0 {
      return InvalidHttp("empty HTTP header name")
    }
    let name = line
      .view(start_offset=0, end_offset=colon)
      .trim()
      .to_owned()
      .to_lower()
    let value = line.view(start_offset=colon + 1).trim().to_owned()
    headers[name] = value
  }
  Parsed({ verb, path, version, headers, body })
}

///|
/// Create a GitHub-compatible HMAC-SHA256 WebHook endpoint.
pub fn new_github_endpoint(
  path : StringView,
  secret : String,
  router : HookRouter,
) -> WebhookEndpoint {
  {
    path: path.to_owned(),
    adapter: github_adapter(),
    secret,
    router,
    middlewares: [HookMiddleware("require-json", require_json_middleware())],
    deduper: new_in_memory_deduper(),
    max_body_bytes: 1024 * 1024,
  }
}

///|
pub fn WebhookEndpoint::use_middleware(
  self : WebhookEndpoint,
  name : StringView,
  callback : MiddlewareFn,
) -> WebhookEndpoint {
  let middlewares : Array[HookMiddleware] = []
  for middleware in self.middlewares {
    middlewares.push(middleware)
  }
  middlewares.push(HookMiddleware(name, callback))
  { ..self, middlewares, }
}

///|
pub fn WebhookEndpoint::with_max_body_bytes(
  self : WebhookEndpoint,
  max_body_bytes : Int,
) -> WebhookEndpoint {
  { ..self, max_body_bytes, }
}

///|
fn endpoint_response(result : HookResult) -> HttpResponse {
  match result.status {
    Accepted => HttpResponse(202, "Accepted", result.message)
    Ignored => HttpResponse(200, "OK", result.message)
    Retried => HttpResponse(503, "Service Unavailable", result.message)
    Failed => HttpResponse(422, "Unprocessable Entity", result.message)
  }
}

///|
/// Handle a structured HTTP request supplied by any MoonBit server runtime.
/// Signature verification happens before payload parsing and routing.
pub fn WebhookEndpoint::handle_http(
  self : WebhookEndpoint,
  http : HttpRequest,
) -> (WebhookEndpoint, HttpResponse) {
  if http.verb != "POST" {
    return (
      self,
      HttpResponse(405, "Method Not Allowed", "WebHook endpoint requires POST"),
    )
  }
  if http.path != self.path {
    return (
      self,
      HttpResponse(404, "Not Found", "WebHook endpoint path does not match"),
    )
  }
  if http.body.length() > self.max_body_bytes {
    return (
      self,
      HttpResponse(
        413, "Payload Too Large", "WebHook body exceeds configured limit",
      ),
    )
  }
  let request = self.adapter.parse_request(http.headers, http.body)
  let verification = verify_github_signature(request, self.secret)
  if verification.status != Verified {
    return (self, HttpResponse(401, "Unauthorized", verification.reason))
  }
  let event = enrich_provider_event(request)
  if !self.deduper.should_process(event) {
    return (
      self,
      HttpResponse(200, "OK", "duplicate delivery " + event.idempotency_key),
    )
  }
  let mut pipeline = new_hook_pipeline((event, _) => self.router.dispatch(event))
  for middleware in self.middlewares {
    pipeline = pipeline.use_middleware(middleware.name, middleware.callback)
  }
  let outcome = pipeline.run(event)
  let next = if outcome.result.status == Accepted {
    { ..self, deduper: self.deduper.remember_event(outcome.event, "processed") }
  } else {
    self
  }
  (next, endpoint_response(outcome.result))
}

///|
/// Parse and handle one raw HTTP/1.x request.
pub fn WebhookEndpoint::handle_raw(
  self : WebhookEndpoint,
  raw : String,
) -> (WebhookEndpoint, HttpResponse) {
  match parse_http_request(raw) {
    Parsed(request) => self.handle_http(request)
    InvalidHttp(reason) => (self, HttpResponse(400, "Bad Request", reason))
  }
}