///|
/// Final decision produced by an upload contract inspection.
pub(all) enum UploadDecision {
  UploadAccepted
  UploadRejected
} derive(Debug, Eq)

///|
/// Application-facing multipart contract.
///
/// A contract combines parser limits, a named form schema, and the highest
/// analysis risk accepted by an endpoint.
pub(all) struct UploadContract {
  parse_options : ParseOptions
  schema : FormSchema
  max_risk : UploadRisk
} derive(Debug, Eq)

///|
/// Complete result of parsing and checking one upload request.
pub(all) struct UploadInspection {
  form : MultipartForm
  validation : ValidationReport
  analysis : FormAnalysis
  max_risk : UploadRisk
  decision : UploadDecision
} derive(Debug, Eq)

///|
/// Create a contract with conservative parser limits and a medium risk ceiling.
pub fn upload_contract(schema : FormSchema) -> UploadContract {
  { parse_options: default_parse_options(), schema, max_risk: MediumRisk }
}

///|
/// Replace the parser limits used by a contract.
pub fn UploadContract::with_parse_options(
  self : UploadContract,
  parse_options : ParseOptions,
) -> UploadContract {
  { parse_options, schema: self.schema, max_risk: self.max_risk }
}

///|
/// Replace the declarative form schema used by a contract.
pub fn UploadContract::with_schema(
  self : UploadContract,
  schema : FormSchema,
) -> UploadContract {
  { parse_options: self.parse_options, schema, max_risk: self.max_risk }
}

///|
/// Set the highest analysis risk accepted by a contract.
pub fn UploadContract::with_max_risk(
  self : UploadContract,
  max_risk : UploadRisk,
) -> UploadContract {
  { parse_options: self.parse_options, schema: self.schema, max_risk }
}

///|
/// Parse and inspect a request using one application-facing upload contract.
pub fn inspect_upload_request(
  request : MultipartRequest,
  contract : UploadContract,
) -> Result[UploadInspection, MultipartError] {
  inspect_upload(request.content_type, request.body, contract)
}

///|
/// Parse and inspect a raw multipart request using one upload contract.
pub fn inspect_upload(
  content_type : String,
  body : String,
  contract : UploadContract,
) -> Result[UploadInspection, MultipartError] {
  match
    parse_multipart_request_with_options(
      content_type,
      body,
      contract.parse_options,
    ) {
    Ok(form) => Ok(inspect_parsed_form(form, contract))
    Err(err) => Err(err)
  }
}

///|
/// Inspect an already parsed form without parsing it again.
pub fn inspect_parsed_form(
  form : MultipartForm,
  contract : UploadContract,
) -> UploadInspection {
  let validation = form.validate_schema(contract.schema)
  let analysis = analyze_form(form)
  let decision = if validation.is_ok() &&
    upload_risk_at_most(analysis.risk, contract.max_risk) {
    UploadAccepted
  } else {
    UploadRejected
  }
  { form, validation, analysis, max_risk: contract.max_risk, decision }
}

///|
pub fn UploadDecision::label(self : UploadDecision) -> String {
  match self {
    UploadAccepted => "accepted"
    UploadRejected => "rejected"
  }
}

///|
pub fn UploadInspection::is_accepted(self : UploadInspection) -> Bool {
  self.decision == UploadAccepted
}

///|
/// A conventional status code for endpoint adapters.
pub fn UploadInspection::status_code(self : UploadInspection) -> Int {
  if self.is_accepted() {
    200
  } else {
    422
  }
}

///|
/// Stable one-line result for logs, CI output, and webhook diagnostics.
pub fn UploadInspection::decision_line(self : UploadInspection) -> String {
  "decision=" +
  self.decision.label() +
  ", risk=" +
  self.analysis.risk.label() +
  ", max_risk=" +
  self.max_risk.label() +
  ", validation_issues=" +
  self.validation.issue_count().to_string() +
  ", parts=" +
  self.form.part_count().to_string()
}

///|
/// Explain every reason why an otherwise parseable request was rejected.
pub fn UploadInspection::rejection_reasons(
  self : UploadInspection,
) -> Array[String] {
  let reasons = Array::new()
  if !self.validation.is_ok() {
    let validation_lines = self.validation.to_lines()
    let mut i = 0
    while i < validation_lines.length() {
      reasons.push(validation_lines[i])
      i = i + 1
    }
  }
  if !upload_risk_at_most(self.analysis.risk, self.max_risk) {
    reasons.push(
      "upload risk " +
      self.analysis.risk.label() +
      " exceeds contract limit " +
      self.max_risk.label(),
    )
  }
  reasons
}

///|
/// Render a deterministic inspection report.
pub fn UploadInspection::to_lines(self : UploadInspection) -> Array[String] {
  let lines = Array::new()
  lines.push(self.decision_line())
  let reasons = self.rejection_reasons()
  let mut i = 0
  while i < reasons.length() {
    lines.push("reason: " + reasons[i])
    i = i + 1
  }
  let analysis_lines = self.analysis.to_lines()
  let mut j = 0
  while j < analysis_lines.length() {
    lines.push("analysis: " + analysis_lines[j])
    j = j + 1
  }
  lines
}

///|
pub fn upload_risk_at_most(actual : UploadRisk, maximum : UploadRisk) -> Bool {
  upload_risk_rank(actual) <= upload_risk_rank(maximum)
}

///|
fn upload_risk_rank(risk : UploadRisk) -> Int {
  match risk {
    LowRisk => 1
    MediumRisk => 2
    HighRisk => 3
  }
}