namespace SeaHaven.Services.Helpers
{
///
/// SH-116 client-confirmed media contract (2026-09-22): photos up to 10 MB (JPEG/PNG/HEIC),
/// videos up to 100 MB and 90 seconds (MP4/MOV), at most 10 photos and 3 videos per work
/// order, across the dispatcher media modal, the completion-doc media tab and the vendor
/// portal upload. Documents (PDF/DOC/DOCX) keep the 50 MB document cap. Duration is only
/// checkable client-side (the server cannot probe it cheaply), so it is enforced in the
/// browser and documented here as part of the same contract.
///
public static class WorkOrderMediaContract
{
public const long MaxPhotoBytes = 10_000_000;
public const long MaxVideoBytes = 100_000_000;
public const long MaxDocumentBytes = 50_000_000;
public const int MaxPhotosPerWorkOrder = 10;
public const int MaxVideosPerWorkOrder = 3;
public const int MaxVideoDurationSeconds = 90;
///
/// Request-level ceiling for media endpoints (RequestSizeLimit / multipart limit). It sits
/// above plus multipart overhead so an oversize file reaches the
/// per-kind check and gets its generic message instead of a framework 413. The EB nginx
/// proxy (.platform/nginx/conf.d/01_upload_body_size.conf) must stay above this value.
///
public const long MaxUploadRequestBytes = 110_000_000;
public enum UploadKind
{
Photo,
Video,
Document,
Unknown
}
private static readonly Dictionary KindByContentType =
new(StringComparer.OrdinalIgnoreCase)
{
["image/jpeg"] = UploadKind.Photo,
["image/jpg"] = UploadKind.Photo,
["image/png"] = UploadKind.Photo,
["image/heic"] = UploadKind.Photo,
["video/mp4"] = UploadKind.Video,
["video/quicktime"] = UploadKind.Video,
["application/pdf"] = UploadKind.Document,
["application/msword"] = UploadKind.Document,
["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] =
UploadKind.Document,
};
private static readonly Dictionary KindByExtension =
new(StringComparer.OrdinalIgnoreCase)
{
[".jpg"] = UploadKind.Photo,
[".jpeg"] = UploadKind.Photo,
[".png"] = UploadKind.Photo,
[".heic"] = UploadKind.Photo,
[".mp4"] = UploadKind.Video,
[".mov"] = UploadKind.Video,
[".pdf"] = UploadKind.Document,
[".doc"] = UploadKind.Document,
[".docx"] = UploadKind.Document,
};
///
/// Classifies an upload. Pass the validated (resolved) content type: it decides whenever
/// it is an allowlisted type, so a misleading file name cannot move a file into a larger
/// size class. The extension only decides when no allowlisted type is known (for example
/// counting stored attachment URLs).
///
public static UploadKind ResolveKind(string? contentType, string? fileName)
{
var type = (contentType ?? string.Empty).Trim();
if (KindByContentType.TryGetValue(type, out var byType))
return byType;
var extension = Path.GetExtension(fileName ?? string.Empty);
return KindByExtension.TryGetValue(extension, out var byExtension)
? byExtension
: UploadKind.Unknown;
}
/// Stable, generic per-kind size message; never echoes file metadata.
public static string? ValidateSize(long length, UploadKind kind)
{
return kind switch
{
UploadKind.Photo when length > MaxPhotoBytes => "Photos must be 10 MB or smaller.",
UploadKind.Video when length > MaxVideoBytes => "Videos must be 100 MB or smaller.",
UploadKind.Document when length > MaxDocumentBytes =>
"Documents must be 50 MB or smaller.",
UploadKind.Unknown when length > MaxVideoBytes =>
"Videos must be 100 MB or smaller.",
_ => null
};
}
public static string? ValidateSize(string? contentType, string? fileName, long length)
=> ValidateSize(length, ResolveKind(contentType, fileName));
}
}