///|
pub fn lint_header(header : String) -> HeaderReview {
  lint_header_with_policy(header, FilenamePolicy::default())
}

///|
pub fn lint_header_with_policy(
  header : String,
  policy : FilenamePolicy,
) -> HeaderReview {
  let recommendations : Array[Recommendation] = []
  match parse_header(header) {
    Ok(disposition) => {
      append_diagnostic_recommendations(
        recommendations,
        disposition.diagnostics,
      )
      append_shape_recommendations(recommendations, disposition)
      let filename = match disposition.filename() {
        Some(value) => {
          let report = sanitize_filename_with_policy(value, policy)
          append_filename_recommendations(recommendations, report)
          Some(report)
        }
        None => None
      }
      {
        header,
        disposition: Some(disposition),
        filename,
        recommendations,
        status: status_for_recommendations(recommendations),
      }
    }
    Err(err) => {
      recommendations.push(
        Recommendation::make(
          FixSyntax,
          Error,
          err.message,
          None,
          None,
          err.offset,
        ),
      )
      {
        header,
        disposition: None,
        filename: None,
        recommendations,
        status: Rejected,
      }
    }
  }
}

///|
pub fn lint_headers(headers : Array[String]) -> Array[HeaderReview] {
  let reviews : Array[HeaderReview] = []
  for h in headers {
    reviews.push(lint_header(h))
  }
  reviews
}

///|
pub fn lint_headers_with_policy(
  headers : Array[String],
  policy : FilenamePolicy,
) -> Array[HeaderReview] {
  let reviews : Array[HeaderReview] = []
  for h in headers {
    reviews.push(lint_header_with_policy(h, policy))
  }
  reviews
}

///|
pub fn summarize_reviews(reviews : Array[HeaderReview]) -> AuditSummary {
  let mut accepted = 0
  let mut needs_attention = 0
  let mut rejected = 0
  let mut warnings = 0
  let mut errors = 0
  let mut filenames_changed = 0
  for review in reviews {
    match review.status {
      Accepted => accepted = accepted + 1
      NeedsAttention => needs_attention = needs_attention + 1
      Rejected => rejected = rejected + 1
    }
    warnings = warnings + review.warning_count()
    errors = errors + review.error_count()
    match review.filename {
      Some(report) =>
        if report.changed {
          filenames_changed = filenames_changed + 1
        }
      None => ()
    }
  }
  {
    total: reviews.length(),
    accepted,
    needs_attention,
    rejected,
    warnings,
    errors,
    filenames_changed,
  }
}

///|
pub fn HeaderReview::is_ok(self : HeaderReview) -> Bool {
  self.status == Accepted
}

///|
pub fn HeaderReview::warning_count(self : HeaderReview) -> Int {
  count_recommendations_by_severity(self.recommendations, Warning)
}

///|
pub fn HeaderReview::error_count(self : HeaderReview) -> Int {
  count_recommendations_by_severity(self.recommendations, Error)
}

///|
pub fn HeaderReview::filename_value(self : HeaderReview) -> String? {
  match self.filename {
    Some(report) => Some(report.value)
    None => None
  }
}

///|
pub fn HeaderReview::disposition_kind(self : HeaderReview) -> String? {
  match self.disposition {
    Some(d) => Some(d.kind.to_token())
    None => None
  }
}

///|
fn append_diagnostic_recommendations(
  recommendations : Array[Recommendation],
  diagnostics : Array[Diagnostic],
) -> Unit {
  for d in diagnostics {
    recommendations.push(recommendation_from_diagnostic(d))
  }
}

///|
fn append_shape_recommendations(
  recommendations : Array[Recommendation],
  disposition : Disposition,
) -> Unit {
  if disposition.is_form_data() && !disposition.has_param("name") {
    recommendations.push(
      Recommendation::make(
        AddMultipartName,
        Warning,
        "Multipart form-data should include a name parameter",
        Some("name"),
        None,
        None,
      ),
    )
  }
  match disposition.filename() {
    Some(value) => {
      if has_non_ascii(value) && disposition.extended_filename() is None {
        recommendations.push(
          Recommendation::make(
            PreferFilenameStar,
            Warning,
            "Non-ASCII filenames should include an RFC 5987 filename* parameter",
            Some("filename*"),
            Some(value),
            None,
          ),
        )
      }
      if disposition.plain_filename() is None &&
        disposition.extended_filename() is Some(_) {
        recommendations.push(
          Recommendation::make(
            AddAsciiFallback,
            Note,
            "An ASCII filename fallback improves compatibility with older clients",
            Some("filename"),
            Some(filename_ascii_fallback(value)),
            None,
          ),
        )
      }
    }
    None => ()
  }
}

///|
fn append_filename_recommendations(
  recommendations : Array[Recommendation],
  report : FilenameReport,
) -> Unit {
  if report.value.is_empty() {
    recommendations.push(
      Recommendation::make(
        AvoidEmptyFilename,
        Error,
        "Filename is empty after normalization",
        Some("filename"),
        None,
        None,
      ),
    )
  } else if report.changed {
    recommendations.push(
      Recommendation::make(
        SanitizeFilename,
        Warning,
        "Filename should be normalized before writing to disk",
        Some("filename"),
        Some(report.value),
        None,
      ),
    )
  }
  for d in report.diagnostics {
    if d.code == LengthReduced {
      recommendations.push(
        Recommendation::make(
          LimitFilenameLength,
          Warning,
          d.message,
          Some("filename"),
          Some(report.value),
          d.offset,
        ),
      )
    }
  }
}

///|
fn recommendation_from_diagnostic(d : Diagnostic) -> Recommendation {
  let code = match d.code {
    DuplicateParameter => RemoveDuplicateParameter
    UnsupportedCharset => UseSupportedCharset
    UnsafeFilename => SanitizeFilename
    PathSegmentStripped => SanitizeFilename
    ReservedName => SanitizeFilename
    EmptyFilename => AvoidEmptyFilename
    LengthReduced => LimitFilenameLength
    _ => FixSyntax
  }
  Recommendation::make(code, d.severity, d.message, None, None, d.offset)
}

///|
fn count_recommendations_by_severity(
  recommendations : Array[Recommendation],
  severity : Severity,
) -> Int {
  let mut count = 0
  for r in recommendations {
    if r.severity == severity {
      count = count + 1
    }
  }
  count
}

///|
fn status_for_recommendations(
  recommendations : Array[Recommendation],
) -> ReviewStatus {
  let mut has_warning = false
  for r in recommendations {
    if r.severity == Error {
      return Rejected
    }
    if r.severity == Warning {
      has_warning = true
    }
  }
  if has_warning {
    NeedsAttention
  } else {
    Accepted
  }
}

///|
fn has_non_ascii(value : String) -> Bool {
  value.any(c => !c.is_ascii())
}