///|
/// Registered and extension disposition kinds from HTTP Content-Disposition.
pub(all) enum DispositionKind {
  Inline
  Attachment
  FormData
  Other(String)
} derive(Eq, Debug)

///|
/// Diagnostic severity used by parser and filename policy checks.
pub(all) enum Severity {
  Note
  Warning
  Error
} derive(Eq, Debug)

///|
/// Overall result produced by header linting.
pub(all) enum ReviewStatus {
  Accepted
  NeedsAttention
  Rejected
} derive(Eq, Debug)

///|
/// Stable machine-readable diagnostic codes.
pub(all) enum DiagnosticCode {
  EmptyHeader
  InvalidDisposition
  InvalidParameterName
  MissingEquals
  MissingValue
  UnterminatedQuote
  BadEscape
  DuplicateParameter
  InvalidPercentEncoding
  InvalidExtendedValue
  UnsupportedCharset
  UnsafeFilename
  PathSegmentStripped
  ReservedName
  EmptyFilename
  LengthReduced
  ControlCharacter
} derive(Eq, Debug)

///|
/// Stable machine-readable recommendation codes.
pub(all) enum RecommendationCode {
  PreferFilenameStar
  AddAsciiFallback
  SanitizeFilename
  RemoveDuplicateParameter
  FixSyntax
  UseSupportedCharset
  AddMultipartName
  AvoidEmptyFilename
  LimitFilenameLength
  PreserveExplicitDisposition
  ReviewFileExtension
  AvoidExecutableDownload
  PreferAttachmentDisposition
  InferFilename
} derive(Eq, Debug)

///|
/// A parser or policy diagnostic with optional UTF-16 offset in the input.
pub(all) struct Diagnostic {
  code : DiagnosticCode
  severity : Severity
  message : String
  offset : Int?
} derive(Eq, Debug)

///|
/// Human-oriented recommendation produced by linting.
pub(all) struct Recommendation {
  code : RecommendationCode
  severity : Severity
  message : String
  parameter : String?
  value : String?
  offset : Int?
} derive(Eq, Debug)

///|
/// Fatal parse error returned by strict entry points.
pub(all) struct ParseError {
  code : DiagnosticCode
  message : String
  offset : Int?
} derive(Eq, Debug)

///|
/// A single Content-Disposition parameter.
///
/// `name` is normalized to lowercase and has the RFC 5987 `*` suffix removed.
/// `extended` records whether the parameter came from `name*=...`.
pub(all) struct Parameter {
  name : String
  value : String
  raw_name : String
  raw_value : String
  extended : Bool
  charset : String?
  language : String?
  position : Int
} derive(Eq, Debug)

///|
/// Parsed header plus recoverable diagnostics.
pub(all) struct Disposition {
  kind : DispositionKind
  params : Array[Parameter]
  diagnostics : Array[Diagnostic]
} derive(Eq, Debug)

///|
/// Result of filename normalization.
pub(all) struct FilenameReport {
  value : String
  original : String
  changed : Bool
  diagnostics : Array[Diagnostic]
} derive(Eq, Debug)

///|
/// Policy for `sanitize_filename_with_policy`.
pub(all) struct FilenamePolicy {
  replacement : String
  max_length : Int
  default_name : String
  allow_hidden : Bool
} derive(Eq, Debug)

///|
/// Lint result for one Content-Disposition header.
pub(all) struct HeaderReview {
  header : String
  disposition : Disposition?
  filename : FilenameReport?
  recommendations : Array[Recommendation]
  status : ReviewStatus
} derive(Eq, Debug)

///|
/// Aggregate counters for a batch of header reviews.
pub(all) struct AuditSummary {
  total : Int
  accepted : Int
  needs_attention : Int
  rejected : Int
  warnings : Int
  errors : Int
  filenames_changed : Int
} derive(Eq, Debug)

///|
/// Policy used to convert a batch review into a CI or gateway decision.
pub(all) struct AuditGatePolicy {
  max_warnings : Int
  max_errors : Int
  allow_needs_attention : Bool
  allow_filename_changes : Bool
  allow_rejected_headers : Bool
} derive(Eq, Debug)

///|
/// Decision produced by applying an `AuditGatePolicy` to review results.
pub(all) struct AuditGateResult {
  accepted : Bool
  status : ReviewStatus
  summary : AuditSummary
  reasons : Array[String]
} derive(Eq, Debug)

///|
pub fn FilenamePolicy::default() -> FilenamePolicy {
  {
    replacement: "_",
    max_length: 120,
    default_name: "download",
    allow_hidden: false,
  }
}

///|
pub fn FilenamePolicy::strict_download() -> FilenamePolicy {
  {
    replacement: "_",
    max_length: 96,
    default_name: "download",
    allow_hidden: false,
  }
}

///|
pub fn FilenamePolicy::object_storage_key() -> FilenamePolicy {
  {
    replacement: "-",
    max_length: 180,
    default_name: "object",
    allow_hidden: true,
  }
}

///|
pub fn FilenamePolicy::multipart_upload() -> FilenamePolicy {
  {
    replacement: "_",
    max_length: 128,
    default_name: "upload",
    allow_hidden: false,
  }
}

///|
pub fn AuditGatePolicy::strict() -> AuditGatePolicy {
  {
    max_warnings: 0,
    max_errors: 0,
    allow_needs_attention: false,
    allow_filename_changes: false,
    allow_rejected_headers: false,
  }
}

///|
pub fn AuditGatePolicy::download_gateway() -> AuditGatePolicy {
  {
    max_warnings: 8,
    max_errors: 0,
    allow_needs_attention: true,
    allow_filename_changes: true,
    allow_rejected_headers: false,
  }
}

///|
pub fn AuditGatePolicy::report_only() -> AuditGatePolicy {
  {
    max_warnings: 1000000,
    max_errors: 1000000,
    allow_needs_attention: true,
    allow_filename_changes: true,
    allow_rejected_headers: true,
  }
}

///|
pub fn AuditGatePolicy::with_max_warnings(
  self : AuditGatePolicy,
  max_warnings : Int,
) -> AuditGatePolicy {
  { ..self, max_warnings, }
}

///|
pub fn AuditGatePolicy::with_max_errors(
  self : AuditGatePolicy,
  max_errors : Int,
) -> AuditGatePolicy {
  { ..self, max_errors, }
}

///|
pub fn AuditGatePolicy::with_needs_attention(
  self : AuditGatePolicy,
  allowed : Bool,
) -> AuditGatePolicy {
  { ..self, allow_needs_attention: allowed }
}

///|
pub fn AuditGatePolicy::with_filename_changes(
  self : AuditGatePolicy,
  allowed : Bool,
) -> AuditGatePolicy {
  { ..self, allow_filename_changes: allowed }
}

///|
pub fn AuditGatePolicy::with_rejected_headers(
  self : AuditGatePolicy,
  allowed : Bool,
) -> AuditGatePolicy {
  { ..self, allow_rejected_headers: allowed }
}

///|
pub fn Diagnostic::make(
  code : DiagnosticCode,
  severity : Severity,
  message : String,
  offset : Int?,
) -> Diagnostic {
  { code, severity, message, offset }
}

///|
pub fn Recommendation::make(
  code : RecommendationCode,
  severity : Severity,
  message : String,
  parameter : String?,
  value : String?,
  offset : Int?,
) -> Recommendation {
  { code, severity, message, parameter, value, offset }
}

///|
pub fn ParseError::from_diagnostic(d : Diagnostic) -> ParseError {
  { code: d.code, message: d.message, offset: d.offset }
}

///|
pub fn Parameter::plain(name : String, value : String) -> Parameter {
  {
    name: normalize_param_name(name),
    value,
    raw_name: name,
    raw_value: value,
    extended: false,
    charset: None,
    language: None,
    position: 0,
  }
}

///|
pub fn Parameter::extended(
  name : String,
  value : String,
  charset? : String = "UTF-8",
  language? : String = "",
) -> Parameter {
  {
    name: normalize_param_name(name),
    value,
    raw_name: name + "*",
    raw_value: encode_ext_value(value, charset~, language~),
    extended: true,
    charset: Some(charset),
    language: Some(language),
    position: 0,
  }
}

///|
pub fn DispositionKind::from_token(token : String) -> DispositionKind {
  let lower = token.to_lower()
  if lower == "inline" {
    Inline
  } else if lower == "attachment" {
    Attachment
  } else if lower == "form-data" {
    FormData
  } else {
    Other(lower)
  }
}

///|
pub fn DispositionKind::to_token(self : DispositionKind) -> String {
  match self {
    Inline => "inline"
    Attachment => "attachment"
    FormData => "form-data"
    Other(name) => name
  }
}

///|
pub fn DiagnosticCode::name(self : DiagnosticCode) -> String {
  match self {
    EmptyHeader => "empty-header"
    InvalidDisposition => "invalid-disposition"
    InvalidParameterName => "invalid-parameter-name"
    MissingEquals => "missing-equals"
    MissingValue => "missing-value"
    UnterminatedQuote => "unterminated-quote"
    BadEscape => "bad-escape"
    DuplicateParameter => "duplicate-parameter"
    InvalidPercentEncoding => "invalid-percent-encoding"
    InvalidExtendedValue => "invalid-extended-value"
    UnsupportedCharset => "unsupported-charset"
    UnsafeFilename => "unsafe-filename"
    PathSegmentStripped => "path-segment-stripped"
    ReservedName => "reserved-name"
    EmptyFilename => "empty-filename"
    LengthReduced => "length-reduced"
    ControlCharacter => "control-character"
  }
}

///|
pub fn RecommendationCode::name(self : RecommendationCode) -> String {
  match self {
    PreferFilenameStar => "prefer-filename-star"
    AddAsciiFallback => "add-ascii-fallback"
    SanitizeFilename => "sanitize-filename"
    RemoveDuplicateParameter => "remove-duplicate-parameter"
    FixSyntax => "fix-syntax"
    UseSupportedCharset => "use-supported-charset"
    AddMultipartName => "add-multipart-name"
    AvoidEmptyFilename => "avoid-empty-filename"
    LimitFilenameLength => "limit-filename-length"
    PreserveExplicitDisposition => "preserve-explicit-disposition"
    ReviewFileExtension => "review-file-extension"
    AvoidExecutableDownload => "avoid-executable-download"
    PreferAttachmentDisposition => "prefer-attachment-disposition"
    InferFilename => "infer-filename"
  }
}

///|
pub fn Severity::name(self : Severity) -> String {
  match self {
    Note => "note"
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn ReviewStatus::name(self : ReviewStatus) -> String {
  match self {
    Accepted => "accepted"
    NeedsAttention => "needs-attention"
    Rejected => "rejected"
  }
}

///|
pub fn Disposition::get_param(self : Disposition, name : String) -> String? {
  self.find_param(name).map(p => p.value)
}

///|
pub fn Disposition::find_param(self : Disposition, name : String) -> Parameter? {
  let wanted = normalize_param_name(name)
  let mut plain : Parameter? = None
  for p in self.params {
    if p.name == wanted {
      if p.extended {
        return Some(p)
      }
      if plain is None {
        plain = Some(p)
      }
    }
  }
  plain
}

///|
pub fn Disposition::plain_filename(self : Disposition) -> String? {
  for p in self.params {
    if p.name == "filename" && !p.extended {
      return Some(p.value)
    }
  }
  None
}

///|
pub fn Disposition::extended_filename(self : Disposition) -> String? {
  for p in self.params {
    if p.name == "filename" && p.extended {
      return Some(p.value)
    }
  }
  None
}

///|
pub fn Disposition::params_named(
  self : Disposition,
  name : String,
) -> Array[Parameter] {
  let wanted = normalize_param_name(name)
  let found : Array[Parameter] = []
  for p in self.params {
    if p.name == wanted {
      found.push(p)
    }
  }
  found
}

///|
pub fn Disposition::has_param(self : Disposition, name : String) -> Bool {
  self.find_param(name) is Some(_)
}

///|
pub fn Disposition::is_attachment(self : Disposition) -> Bool {
  self.kind == Attachment
}

///|
pub fn Disposition::is_inline(self : Disposition) -> Bool {
  self.kind == Inline
}

///|
pub fn Disposition::is_form_data(self : Disposition) -> Bool {
  self.kind == FormData
}

///|
pub fn Disposition::filename(self : Disposition) -> String? {
  self.get_param("filename")
}

///|
pub fn Disposition::name(self : Disposition) -> String? {
  self.get_param("name")
}

///|
pub fn Disposition::has_errors(self : Disposition) -> Bool {
  self.diagnostics.any(d => d.severity == Error)
}

///|
pub fn Disposition::to_header(self : Disposition) -> String {
  serialize(self)
}

///|
pub fn Parameter::is_filename(self : Parameter) -> Bool {
  self.name == "filename"
}

///|
pub fn Parameter::is_name(self : Parameter) -> Bool {
  self.name == "name"
}

///|
pub fn Parameter::effective_name(self : Parameter) -> String {
  if self.extended {
    self.name + "*"
  } else {
    self.name
  }
}

///|
pub fn normalize_param_name(name : String) -> String {
  let trimmed = name.trim(chars=" \t").to_owned()
  let without_star = if trimmed.has_suffix("*") && trimmed.length() > 0 {
    trimmed[0:trimmed.length() - 1].to_owned()
  } else {
    trimmed
  }
  without_star.to_lower()
}