///|
/// Validation and lookup errors produced by an upload contract catalog.
pub(all) enum CatalogError {
  InvalidEndpointId(String)
  InvalidEndpointMethod(String)
  InvalidEndpointPath(String)
  InvalidEndpointVersion(String)
  DuplicateEndpointId(String)
  DuplicateEndpointRoute(String)
  EndpointNotFound(String)
} derive(Debug, Eq)

///|
/// Request inspection errors distinguish routing failures from multipart failures.
pub(all) enum CatalogInspectionError {
  CatalogRoutingError(CatalogError)
  CatalogUploadError(MultipartError)
} derive(Debug, Eq)

///|
/// One versioned upload endpoint and its application-facing contract.
pub(all) struct UploadEndpoint {
  id : String
  http_method : String
  path : String
  version : String
  contract : UploadContract
} derive(Debug, Eq)

///|
/// Immutable-style collection of upload endpoint contracts.
pub(all) struct UploadContractCatalog {
  endpoints : Array[UploadEndpoint]
} derive(Debug, Eq)

///|
/// Successful routed inspection with endpoint identity preserved.
pub(all) struct CatalogInspection {
  endpoint_id : String
  endpoint_version : String
  inspection : UploadInspection
} derive(Debug, Eq)

///|
/// Named conformance suite assigned to one catalog endpoint.
pub(all) struct EndpointConformancePlan {
  endpoint_id : String
  suite_name : String
  cases : Array[UploadConformanceCase]
} derive(Debug, Eq)

///|
/// Multi-endpoint conformance result for CI and API governance.
pub(all) struct CatalogConformanceReport {
  catalog_name : String
  suites : Array[ConformanceSuiteResult]
  issues : Array[String]
} derive(Debug, Eq)

///|
/// Create and validate one endpoint catalog entry.
pub fn upload_endpoint(
  id : String,
  http_method : String,
  path : String,
  version : String,
  contract : UploadContract,
) -> Result[UploadEndpoint, CatalogError] {
  let normalized_id = trim_ascii(id)
  let normalized_method = catalog_upper_ascii(trim_ascii(http_method))
  let normalized_path = trim_ascii(path)
  let normalized_version = trim_ascii(version)
  if normalized_id == "" {
    return Err(InvalidEndpointId(id))
  }
  if !catalog_valid_method(normalized_method) {
    return Err(InvalidEndpointMethod(http_method))
  }
  if !catalog_valid_path(normalized_path) {
    return Err(InvalidEndpointPath(path))
  }
  if normalized_version == "" {
    return Err(InvalidEndpointVersion(version))
  }
  Ok({
    id: normalized_id,
    http_method: normalized_method,
    path: normalized_path,
    version: normalized_version,
    contract,
  })
}

///|
/// Create an empty upload contract catalog.
pub fn upload_contract_catalog() -> UploadContractCatalog {
  { endpoints: [] }
}

///|
/// Add one endpoint while rejecting duplicate IDs and routes.
pub fn UploadContractCatalog::add(
  self : UploadContractCatalog,
  endpoint : UploadEndpoint,
) -> Result[UploadContractCatalog, CatalogError] {
  let mut i = 0
  while i < self.endpoints.length() {
    if self.endpoints[i].id == endpoint.id {
      return Err(DuplicateEndpointId(endpoint.id))
    }
    if self.endpoints[i].http_method == endpoint.http_method &&
      self.endpoints[i].path == endpoint.path {
      return Err(
        DuplicateEndpointRoute(endpoint.http_method + " " + endpoint.path),
      )
    }
    i = i + 1
  }
  let endpoints = catalog_clone_endpoints(self.endpoints)
  endpoints.push(endpoint)
  Ok({ endpoints, })
}

///|
/// Return an endpoint by stable catalog ID.
pub fn UploadContractCatalog::find_by_id(
  self : UploadContractCatalog,
  id : String,
) -> UploadEndpoint? {
  let normalized = trim_ascii(id)
  let mut i = 0
  while i < self.endpoints.length() {
    if self.endpoints[i].id == normalized {
      return Some(self.endpoints[i])
    }
    i = i + 1
  }
  None
}

///|
/// Return an endpoint by normalized HTTP method and exact path.
pub fn UploadContractCatalog::find_route(
  self : UploadContractCatalog,
  http_method : String,
  path : String,
) -> UploadEndpoint? {
  let normalized_method = catalog_upper_ascii(trim_ascii(http_method))
  let normalized_path = trim_ascii(path)
  let mut i = 0
  while i < self.endpoints.length() {
    if self.endpoints[i].http_method == normalized_method &&
      self.endpoints[i].path == normalized_path {
      return Some(self.endpoints[i])
    }
    i = i + 1
  }
  None
}

///|
/// Route and inspect a multipart request using the matching endpoint contract.
pub fn UploadContractCatalog::inspect_route(
  self : UploadContractCatalog,
  http_method : String,
  path : String,
  request : MultipartRequest,
) -> Result[CatalogInspection, CatalogInspectionError] {
  match self.find_route(http_method, path) {
    Some(endpoint) =>
      match inspect_upload_request(request, endpoint.contract) {
        Ok(inspection) =>
          Ok({
            endpoint_id: endpoint.id,
            endpoint_version: endpoint.version,
            inspection,
          })
        Err(err) => Err(CatalogUploadError(err))
      }
    None =>
      Err(
        CatalogRoutingError(
          EndpointNotFound(
            catalog_upper_ascii(trim_ascii(http_method)) +
            " " +
            trim_ascii(path),
          ),
        ),
      )
  }
}

///|
/// Create a conformance plan for one endpoint ID.
pub fn endpoint_conformance_plan(
  endpoint_id : String,
  suite_name : String,
  cases : Array[UploadConformanceCase],
) -> EndpointConformancePlan {
  {
    endpoint_id: trim_ascii(endpoint_id),
    suite_name: trim_ascii(suite_name),
    cases: catalog_clone_cases(cases),
  }
}

///|
/// Run conformance plans against every referenced endpoint in a catalog.
pub fn run_catalog_conformance(
  catalog_name : String,
  catalog : UploadContractCatalog,
  plans : Array[EndpointConformancePlan],
) -> CatalogConformanceReport {
  let suites = Array::new()
  let issues = Array::new()
  let suite_keys : Array[String] = []
  let mut i = 0
  while i < plans.length() {
    let plan = plans[i]
    let suite_key = plan.endpoint_id + ":" + plan.suite_name
    if catalog_string_contains(suite_keys, suite_key) {
      issues.push("duplicate endpoint suite: " + suite_key)
    } else {
      suite_keys.push(suite_key)
      match catalog.find_by_id(plan.endpoint_id) {
        Some(endpoint) =>
          suites.push(
            run_conformance_suite(
              endpoint.id + "@" + endpoint.version + "/" + plan.suite_name,
              endpoint.contract,
              plan.cases,
            ),
          )
        None => issues.push("endpoint not found for suite: " + plan.endpoint_id)
      }
    }
    i = i + 1
  }
  { catalog_name: trim_ascii(catalog_name), suites, issues }
}

///|
pub fn CatalogError::message(self : CatalogError) -> String {
  match self {
    InvalidEndpointId(value) => "invalid endpoint id: " + value
    InvalidEndpointMethod(value) => "invalid endpoint method: " + value
    InvalidEndpointPath(value) => "invalid endpoint path: " + value
    InvalidEndpointVersion(value) => "invalid endpoint version: " + value
    DuplicateEndpointId(value) => "duplicate endpoint id: " + value
    DuplicateEndpointRoute(value) => "duplicate endpoint route: " + value
    EndpointNotFound(value) => "upload endpoint not found: " + value
  }
}

///|
pub fn CatalogInspectionError::message(self : CatalogInspectionError) -> String {
  match self {
    CatalogRoutingError(err) => err.message()
    CatalogUploadError(err) => err.message()
  }
}

///|
pub fn UploadEndpoint::route_key(self : UploadEndpoint) -> String {
  self.http_method + " " + self.path
}

///|
pub fn UploadEndpoint::label(self : UploadEndpoint) -> String {
  self.id + "@" + self.version + " (" + self.route_key() + ")"
}

///|
pub fn UploadContractCatalog::endpoint_count(
  self : UploadContractCatalog,
) -> Int {
  self.endpoints.length()
}

///|
pub fn UploadContractCatalog::to_lines(
  self : UploadContractCatalog,
) -> Array[String] {
  let lines = [
    "upload-contract-catalog: endpoints=" + self.endpoints.length().to_string(),
  ]
  let mut i = 0
  while i < self.endpoints.length() {
    lines.push(self.endpoints[i].label())
    i = i + 1
  }
  lines
}

///|
pub fn CatalogInspection::decision_line(self : CatalogInspection) -> String {
  "endpoint=" +
  self.endpoint_id +
  "@" +
  self.endpoint_version +
  ", " +
  self.inspection.decision_line()
}

///|
pub fn CatalogConformanceReport::passed_count(
  self : CatalogConformanceReport,
) -> Int {
  let mut count = 0
  let mut i = 0
  while i < self.suites.length() {
    count = count + self.suites[i].passed_count()
    i = i + 1
  }
  count
}

///|
pub fn CatalogConformanceReport::failed_count(
  self : CatalogConformanceReport,
) -> Int {
  let mut count = self.issues.length()
  let mut i = 0
  while i < self.suites.length() {
    count = count + self.suites[i].failed_count()
    i = i + 1
  }
  count
}

///|
pub fn CatalogConformanceReport::case_count(
  self : CatalogConformanceReport,
) -> Int {
  let mut count = 0
  let mut i = 0
  while i < self.suites.length() {
    count = count + self.suites[i].cases.length()
    i = i + 1
  }
  count
}

///|
pub fn CatalogConformanceReport::is_ok(self : CatalogConformanceReport) -> Bool {
  self.failed_count() == 0
}

///|
pub fn CatalogConformanceReport::summary(
  self : CatalogConformanceReport,
) -> String {
  "catalog-conformance=" +
  self.catalog_name +
  ", suites=" +
  self.suites.length().to_string() +
  ", cases=" +
  self.case_count().to_string() +
  ", passed=" +
  self.passed_count().to_string() +
  ", failed=" +
  self.failed_count().to_string()
}

///|
/// Render a multi-endpoint Markdown conformance matrix.
pub fn CatalogConformanceReport::to_markdown(
  self : CatalogConformanceReport,
) -> String {
  let lines = [
    "# Upload catalog conformance: " + catalog_markdown_cell(self.catalog_name),
    "",
    self.summary(),
    "",
    "| Suite | Cases | Passed | Failed | Status |",
    "| --- | ---: | ---: | ---: | --- |",
  ]
  let mut i = 0
  while i < self.suites.length() {
    let suite = self.suites[i]
    let status = if suite.is_ok() { "pass" } else { "fail" }
    lines.push(
      "| " +
      catalog_markdown_cell(suite.name) +
      " | " +
      suite.cases.length().to_string() +
      " | " +
      suite.passed_count().to_string() +
      " | " +
      suite.failed_count().to_string() +
      " | " +
      status +
      " |",
    )
    i = i + 1
  }
  if self.issues.length() > 0 {
    lines.push("")
    lines.push("## Catalog issues")
    lines.push("")
    let mut j = 0
    while j < self.issues.length() {
      lines.push("- " + self.issues[j])
      j = j + 1
    }
  }
  lines.join("\n")
}

///|
fn catalog_valid_method(value : String) -> Bool {
  if value == "" {
    return false
  }
  let mut i = 0
  while i < value.length() {
    let code = value.code_unit_at(i)
    if code < 65 || code > 90 {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn catalog_valid_path(value : String) -> Bool {
  if value == "" || !value.has_prefix("/") {
    return false
  }
  value.find("?") is None && value.find("#") is None && value.find(" ") is None
}

///|
fn catalog_upper_ascii(value : String) -> String {
  let out = StringBuilder::new()
  let mut i = 0
  while i < value.length() {
    let code = value.code_unit_at(i)
    if code >= 97 && code <= 122 {
      out.write_char((code.to_int() - 32).unsafe_to_char())
    } else {
      out.write_char(code.unsafe_to_char())
    }
    i = i + 1
  }
  out.to_string()
}

///|
fn catalog_clone_endpoints(
  endpoints : Array[UploadEndpoint],
) -> Array[UploadEndpoint] {
  let copy = Array::new()
  let mut i = 0
  while i < endpoints.length() {
    copy.push(endpoints[i])
    i = i + 1
  }
  copy
}

///|
fn catalog_clone_cases(
  cases : Array[UploadConformanceCase],
) -> Array[UploadConformanceCase] {
  let copy = Array::new()
  let mut i = 0
  while i < cases.length() {
    copy.push(cases[i])
    i = i + 1
  }
  copy
}

///|
fn catalog_string_contains(values : Array[String], target : String) -> Bool {
  let mut i = 0
  while i < values.length() {
    if values[i] == target {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn catalog_markdown_cell(value : String) -> String {
  let out = StringBuilder::new()
  let mut i = 0
  while i < value.length() {
    let code = value.code_unit_at(i)
    if code == 124 {
      out.write_string("\\|")
    } else if code == 10 || code == 13 {
      out.write_char(' ')
    } else {
      out.write_char(code.unsafe_to_char())
    }
    i = i + 1
  }
  out.to_string()
}