///|
pub(all) enum FileKind {
  NoExtension
  TextFile
  DocumentFile
  SpreadsheetFile
  PresentationFile
  ImageFile
  AudioFile
  VideoFile
  ArchiveFile
  CodeFile
  ExecutableFile
  FontFile
  DataFile
  UnknownFile
} derive(Eq, Debug)

///|
pub(all) enum ExtensionRisk {
  LowRisk
  MediumRisk
  HighRisk
  UnknownRisk
} derive(Eq, Debug)

///|
pub(all) struct DownloadRequest {
  filename : String?
  content_type : String?
  prefer_inline : Bool
} derive(Eq, Debug)

///|
pub(all) struct PreparedDownload {
  header : String
  disposition : Disposition
  filename : FilenameReport
  content_type : String?
  extension : String?
  file_kind : FileKind
  risk : ExtensionRisk
  recommendations : Array[Recommendation]
} derive(Eq, Debug)

///|
pub fn DownloadRequest::attachment(filename : String) -> DownloadRequest {
  { filename: Some(filename), content_type: None, prefer_inline: false }
}

///|
pub fn DownloadRequest::inline_preview(
  filename : String,
  content_type? : String,
) -> DownloadRequest {
  { filename: Some(filename), content_type, prefer_inline: true }
}

///|
pub fn DownloadRequest::from_content_type(
  content_type : String,
) -> DownloadRequest {
  { filename: None, content_type: Some(content_type), prefer_inline: false }
}

///|
pub fn prepare_download(request : DownloadRequest) -> PreparedDownload {
  let suggested = match request.filename {
    Some(name) => name
    None => infer_filename_from_content_type(request.content_type)
  }
  let policy = FilenamePolicy::strict_download()
  let filename = sanitize_filename_preserve_extension(suggested, policy)
  let extension = filename_extension(filename.value)
  let file_kind = classify_filename(filename.value)
  let risk = extension_risk(file_kind, extension)
  let disposition_kind = if should_allow_inline(
      request.prefer_inline,
      file_kind,
      risk,
    ) {
    Inline
  } else {
    Attachment
  }
  let disposition = DispositionBuilder::new(disposition_kind)
    .with_filename_policy(suggested, policy)
    .build()
  let recommendations = prepare_download_recommendations(
    request, filename, file_kind, risk, disposition_kind,
  )
  {
    header: disposition.to_header(),
    disposition,
    filename,
    content_type: normalized_content_type_option(request.content_type),
    extension,
    file_kind,
    risk,
    recommendations,
  }
}

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

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

///|
pub fn PreparedDownload::has_high_risk_extension(
  self : PreparedDownload,
) -> Bool {
  self.risk == HighRisk
}

///|
pub fn FileKind::name(self : FileKind) -> String {
  match self {
    NoExtension => "no-extension"
    TextFile => "text"
    DocumentFile => "document"
    SpreadsheetFile => "spreadsheet"
    PresentationFile => "presentation"
    ImageFile => "image"
    AudioFile => "audio"
    VideoFile => "video"
    ArchiveFile => "archive"
    CodeFile => "code"
    ExecutableFile => "executable"
    FontFile => "font"
    DataFile => "data"
    UnknownFile => "unknown"
  }
}

///|
pub fn ExtensionRisk::name(self : ExtensionRisk) -> String {
  match self {
    LowRisk => "low"
    MediumRisk => "medium"
    HighRisk => "high"
    UnknownRisk => "unknown"
  }
}

///|
pub fn normalize_content_type(content_type : String) -> String {
  let base = match content_type.split_once(";") {
    Some((first, _)) => first.to_owned()
    None => content_type
  }
  base.trim(chars=" \t\r\n").to_owned().to_lower()
}

///|
pub fn extension_for_content_type(content_type : String) -> String? {
  let normalized = normalize_content_type(content_type)
  if normalized == "text/plain" {
    Some("txt")
  } else if normalized == "text/csv" {
    Some("csv")
  } else if normalized == "text/html" {
    Some("html")
  } else if normalized == "text/markdown" {
    Some("md")
  } else if normalized == "application/json" {
    Some("json")
  } else if normalized == "application/xml" || normalized == "text/xml" {
    Some("xml")
  } else if normalized == "application/pdf" {
    Some("pdf")
  } else if normalized == "application/zip" {
    Some("zip")
  } else if normalized == "application/gzip" {
    Some("gz")
  } else if normalized == "application/x-tar" {
    Some("tar")
  } else if normalized == "image/png" {
    Some("png")
  } else if normalized == "image/jpeg" {
    Some("jpg")
  } else if normalized == "image/gif" {
    Some("gif")
  } else if normalized == "image/webp" {
    Some("webp")
  } else if normalized == "image/svg+xml" {
    Some("svg")
  } else if normalized == "audio/mpeg" {
    Some("mp3")
  } else if normalized == "audio/wav" || normalized == "audio/wave" {
    Some("wav")
  } else if normalized == "audio/ogg" {
    Some("ogg")
  } else if normalized == "video/mp4" {
    Some("mp4")
  } else if normalized == "video/webm" {
    Some("webm")
  } else if normalized == "application/vnd.ms-excel" {
    Some("xls")
  } else if normalized ==
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
    Some("xlsx")
  } else if normalized == "application/msword" {
    Some("doc")
  } else if normalized ==
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document" {
    Some("docx")
  } else if normalized == "application/vnd.ms-powerpoint" {
    Some("ppt")
  } else if normalized ==
    "application/vnd.openxmlformats-officedocument.presentationml.presentation" {
    Some("pptx")
  } else if normalized == "application/wasm" {
    Some("wasm")
  } else if normalized == "application/octet-stream" {
    Some("bin")
  } else {
    None
  }
}

///|
pub fn content_type_for_extension(extension : String) -> String? {
  let ext = normalize_extension(extension)
  if ext == "txt" {
    Some("text/plain")
  } else if ext == "csv" {
    Some("text/csv")
  } else if ext == "html" || ext == "htm" {
    Some("text/html")
  } else if ext == "md" || ext == "markdown" {
    Some("text/markdown")
  } else if ext == "json" {
    Some("application/json")
  } else if ext == "xml" {
    Some("application/xml")
  } else if ext == "pdf" {
    Some("application/pdf")
  } else if ext == "zip" {
    Some("application/zip")
  } else if ext == "gz" {
    Some("application/gzip")
  } else if ext == "tar" {
    Some("application/x-tar")
  } else if ext == "png" {
    Some("image/png")
  } else if ext == "jpg" || ext == "jpeg" {
    Some("image/jpeg")
  } else if ext == "gif" {
    Some("image/gif")
  } else if ext == "webp" {
    Some("image/webp")
  } else if ext == "svg" {
    Some("image/svg+xml")
  } else if ext == "mp3" {
    Some("audio/mpeg")
  } else if ext == "wav" {
    Some("audio/wav")
  } else if ext == "ogg" {
    Some("audio/ogg")
  } else if ext == "mp4" {
    Some("video/mp4")
  } else if ext == "webm" {
    Some("video/webm")
  } else if ext == "xls" {
    Some("application/vnd.ms-excel")
  } else if ext == "xlsx" {
    Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
  } else if ext == "doc" {
    Some("application/msword")
  } else if ext == "docx" {
    Some(
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    )
  } else if ext == "ppt" {
    Some("application/vnd.ms-powerpoint")
  } else if ext == "pptx" {
    Some(
      "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    )
  } else if ext == "wasm" {
    Some("application/wasm")
  } else if ext == "bin" {
    Some("application/octet-stream")
  } else {
    None
  }
}

///|
pub fn infer_filename_from_content_type(content_type : String?) -> String {
  match content_type {
    Some(value) =>
      match extension_for_content_type(value) {
        Some(ext) => "download." + ext
        None => "download"
      }
    None => "download"
  }
}

///|
pub fn classify_filename(filename : String) -> FileKind {
  match filename_extension(filename) {
    Some(ext) => classify_extension(ext)
    None => NoExtension
  }
}

///|
pub fn classify_extension(extension : String) -> FileKind {
  let ext = normalize_extension(extension)
  if ext.is_empty() {
    NoExtension
  } else if is_text_extension(ext) {
    TextFile
  } else if is_document_extension(ext) {
    DocumentFile
  } else if is_spreadsheet_extension(ext) {
    SpreadsheetFile
  } else if is_presentation_extension(ext) {
    PresentationFile
  } else if is_image_extension(ext) {
    ImageFile
  } else if is_audio_extension(ext) {
    AudioFile
  } else if is_video_extension(ext) {
    VideoFile
  } else if is_archive_extension(ext) {
    ArchiveFile
  } else if is_code_extension(ext) {
    CodeFile
  } else if is_executable_extension(ext) {
    ExecutableFile
  } else if is_font_extension(ext) {
    FontFile
  } else if is_data_extension(ext) {
    DataFile
  } else {
    UnknownFile
  }
}

///|
pub fn extension_risk(kind : FileKind, extension : String?) -> ExtensionRisk {
  match kind {
    NoExtension => UnknownRisk
    TextFile => LowRisk
    DocumentFile => MediumRisk
    SpreadsheetFile => MediumRisk
    PresentationFile => MediumRisk
    ImageFile => LowRisk
    AudioFile => LowRisk
    VideoFile => LowRisk
    ArchiveFile => MediumRisk
    CodeFile => MediumRisk
    ExecutableFile => HighRisk
    FontFile => LowRisk
    DataFile => risk_for_data_extension(extension)
    UnknownFile => UnknownRisk
  }
}

///|
pub fn inline_allowed_for_kind(kind : FileKind) -> Bool {
  match kind {
    TextFile => true
    ImageFile => true
    AudioFile => true
    VideoFile => true
    FontFile => true
    _ => false
  }
}

///|
fn should_allow_inline(
  prefer_inline : Bool,
  kind : FileKind,
  risk : ExtensionRisk,
) -> Bool {
  prefer_inline && inline_allowed_for_kind(kind) && risk != HighRisk
}

///|
fn normalized_content_type_option(content_type : String?) -> String? {
  match content_type {
    Some(value) => Some(normalize_content_type(value))
    None => None
  }
}

///|
fn prepare_download_recommendations(
  request : DownloadRequest,
  filename : FilenameReport,
  kind : FileKind,
  risk : ExtensionRisk,
  disposition_kind : DispositionKind,
) -> Array[Recommendation] {
  let recommendations : Array[Recommendation] = []
  if request.filename is None {
    recommendations.push(
      Recommendation::make(
        InferFilename,
        Note,
        "Filename was inferred from the Content-Type value",
        Some("filename"),
        Some(filename.value),
        None,
      ),
    )
  }
  if filename.changed {
    recommendations.push(
      Recommendation::make(
        SanitizeFilename,
        Warning,
        "Download filename was normalized before header generation",
        Some("filename"),
        Some(filename.value),
        None,
      ),
    )
  }
  if risk == HighRisk {
    recommendations.push(
      Recommendation::make(
        AvoidExecutableDownload,
        Warning,
        "Executable file extensions should be reviewed before download",
        Some("filename"),
        Some(filename.value),
        None,
      ),
    )
  } else if risk == MediumRisk || risk == UnknownRisk {
    recommendations.push(
      Recommendation::make(
        ReviewFileExtension,
        Note,
        "File extension should be reviewed for the deployment context",
        Some("filename"),
        Some(kind.name()),
        None,
      ),
    )
  }
  if request.prefer_inline && disposition_kind == Attachment {
    recommendations.push(
      Recommendation::make(
        PreferAttachmentDisposition,
        Note,
        "Attachment disposition was selected because inline preview is not appropriate",
        None,
        None,
        None,
      ),
    )
  }
  recommendations
}

///|
fn risk_for_data_extension(extension : String?) -> ExtensionRisk {
  match extension {
    Some(value) => {
      let ext = normalize_extension(value)
      if ext == "wasm" {
        MediumRisk
      } else if ext == "bin" || ext == "dat" || ext == "db" {
        UnknownRisk
      } else {
        LowRisk
      }
    }
    None => UnknownRisk
  }
}

///|
fn is_text_extension(ext : String) -> Bool {
  ext == "txt" ||
  ext == "text" ||
  ext == "log" ||
  ext == "md" ||
  ext == "markdown" ||
  ext == "rst" ||
  ext == "csv" ||
  ext == "tsv" ||
  ext == "ini" ||
  ext == "conf" ||
  ext == "cfg" ||
  ext == "properties"
}

///|
fn is_document_extension(ext : String) -> Bool {
  ext == "pdf" ||
  ext == "doc" ||
  ext == "docx" ||
  ext == "odt" ||
  ext == "rtf" ||
  ext == "tex" ||
  ext == "typ" ||
  ext == "epub" ||
  ext == "pages"
}

///|
fn is_spreadsheet_extension(ext : String) -> Bool {
  ext == "xls" ||
  ext == "xlsx" ||
  ext == "ods" ||
  ext == "numbers" ||
  ext == "parquet" ||
  ext == "orc"
}

///|
fn is_presentation_extension(ext : String) -> Bool {
  ext == "ppt" || ext == "pptx" || ext == "odp" || ext == "key"
}

///|
fn is_image_extension(ext : String) -> Bool {
  ext == "png" ||
  ext == "jpg" ||
  ext == "jpeg" ||
  ext == "gif" ||
  ext == "webp" ||
  ext == "bmp" ||
  ext == "tiff" ||
  ext == "tif" ||
  ext == "svg" ||
  ext == "avif" ||
  ext == "ico"
}

///|
fn is_audio_extension(ext : String) -> Bool {
  ext == "mp3" ||
  ext == "wav" ||
  ext == "ogg" ||
  ext == "flac" ||
  ext == "aac" ||
  ext == "m4a" ||
  ext == "opus" ||
  ext == "weba"
}

///|
fn is_video_extension(ext : String) -> Bool {
  ext == "mp4" ||
  ext == "webm" ||
  ext == "mov" ||
  ext == "m4v" ||
  ext == "avi" ||
  ext == "mkv" ||
  ext == "ogv"
}

///|
fn is_archive_extension(ext : String) -> Bool {
  ext == "zip" ||
  ext == "tar" ||
  ext == "gz" ||
  ext == "tgz" ||
  ext == "bz2" ||
  ext == "xz" ||
  ext == "7z" ||
  ext == "rar" ||
  ext == "jar" ||
  ext == "war"
}

///|
fn is_code_extension(ext : String) -> Bool {
  ext == "mbt" ||
  ext == "js" ||
  ext == "ts" ||
  ext == "jsx" ||
  ext == "tsx" ||
  ext == "py" ||
  ext == "rs" ||
  ext == "go" ||
  ext == "java" ||
  ext == "c" ||
  ext == "h" ||
  ext == "cc" ||
  ext == "cpp" ||
  ext == "hpp" ||
  ext == "cs" ||
  ext == "scala" ||
  ext == "kt" ||
  ext == "swift" ||
  ext == "sh" ||
  ext == "bat" ||
  ext == "ps1" ||
  ext == "sql" ||
  ext == "lua" ||
  ext == "rb" ||
  ext == "php" ||
  ext == "erl" ||
  ext == "ex" ||
  ext == "exs"
}

///|
fn is_executable_extension(ext : String) -> Bool {
  ext == "exe" ||
  ext == "dll" ||
  ext == "msi" ||
  ext == "com" ||
  ext == "scr" ||
  ext == "cmd" ||
  ext == "app" ||
  ext == "deb" ||
  ext == "rpm" ||
  ext == "apk" ||
  ext == "dmg" ||
  ext == "pkg"
}

///|
fn is_font_extension(ext : String) -> Bool {
  ext == "ttf" ||
  ext == "otf" ||
  ext == "woff" ||
  ext == "woff2" ||
  ext == "eot"
}

///|
fn is_data_extension(ext : String) -> Bool {
  ext == "json" ||
  ext == "xml" ||
  ext == "yaml" ||
  ext == "yml" ||
  ext == "toml" ||
  ext == "bin" ||
  ext == "dat" ||
  ext == "db" ||
  ext == "sqlite" ||
  ext == "sqlite3" ||
  ext == "wasm"
}