using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using PowderCoating.Application.Configuration;
using PowderCoating.Application.Interfaces;
namespace PowderCoating.Application.Services;
///
/// Manages company logo files stored in Azure Blob Storage.
/// Logos are stored in the companylogos container using the path
/// {companyId}/company-logo{ext} (e.g. 7/company-logo.png).
/// The service enforces a 10 MB size limit and restricts uploads to common
/// web-safe image formats. It also handles the "replace" case by deleting
/// blobs of every other allowed extension before writing the new one, so a
/// company can never end up with multiple active logos at different paths.
///
public class CompanyLogoService : ICompanyLogoService
{
private readonly IAzureBlobStorageService _blobService;
private readonly StorageSettings _settings;
/// Maximum logo file size accepted on upload (10 MB).
private const long MaxFileSize = 10 * 1024 * 1024; // 10 MB
///
/// Exhaustive list of image extensions permitted for company logos.
/// SVG is included because some companies upload vector-format brand assets.
///
private static readonly string[] AllowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"];
///
/// Initialises the service with the blob storage provider and storage
/// configuration (container names, etc.).
///
public CompanyLogoService(
IAzureBlobStorageService blobService,
IOptions settings)
{
_blobService = blobService;
_settings = settings.Value;
}
///
/// Returns the deterministic blob path for a company logo given its file
/// extension. Using a fixed name (rather than a GUID) allows the path to
/// be derived from companyId alone — useful for PDF embedding and
/// sidebar rendering without an extra DB lookup.
///
/// The tenant company's database ID.
/// File extension including the leading dot (e.g. .png).
/// Blob-relative path such as 7/company-logo.png.
public string GetCompanyLogoPath(int companyId, string extension)
{
return $"{companyId}/company-logo{extension}";
}
///
/// Validates and uploads a new company logo, replacing any existing logo
/// regardless of its extension. Old blobs at alternative extensions are
/// deleted first so the container never holds stale copies.
///
/// The uploaded file from the HTTP request.
/// The tenant company's database ID.
///
/// A tuple indicating success, the stored blob path (on success), and a
/// human-readable error message (on failure).
///
public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId)
{
var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize);
if (!isValid)
return (false, string.Empty, error);
// Delete old logo (any extension) before saving new one
await DeleteOldLogosAsync(companyId, extension);
var blobName = GetCompanyLogoPath(companyId, extension);
var contentType = BlobFileHelper.GetContentType(extension);
using var stream = file.OpenReadStream();
var result = await _blobService.UploadAsync(_settings.Containers.CompanyLogos, blobName, stream, contentType);
if (!result.Success)
return (false, string.Empty, result.ErrorMessage);
return (true, blobName, string.Empty);
}
///
/// Deletes the company logo blob at the given path from Azure Blob Storage.
///
/// Blob-relative path previously returned by .
/// Success flag and an error message on failure.
public async Task<(bool Success, string ErrorMessage)> DeleteCompanyLogoAsync(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return (false, "File path is empty");
return await _blobService.DeleteAsync(_settings.Containers.CompanyLogos, filePath);
}
///
/// Downloads the raw bytes of the company logo for embedding in PDFs or
/// serving through the /CompanyLogo controller endpoint.
///
/// Blob-relative path of the logo.
///
/// A tuple with a success flag, the raw file bytes, the MIME content type,
/// and an error message on failure.
///
public async Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetCompanyLogoAsync(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return (false, Array.Empty(), string.Empty, "File path is empty");
return await _blobService.DownloadAsync(_settings.Containers.CompanyLogos, filePath);
}
///
/// Checks whether a logo blob exists at the given path without downloading it.
/// Used by the sidebar to decide whether to show the tenant logo or fall back
/// to the default PCL logo.
///
/// Blob-relative path to check.
/// true if the blob exists; otherwise false.
public async Task CompanyLogoExistsAsync(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return false;
return await _blobService.ExistsAsync(_settings.Containers.CompanyLogos, filePath);
}
///
/// Deletes any pre-existing logo blobs at extensions other than
/// so that uploading a PNG logo
/// automatically removes a previously stored JPG logo (and vice versa).
/// Errors are intentionally swallowed — a missing old blob is not a failure.
///
/// The tenant company's database ID.
///
/// The extension of the incoming upload; blobs with this extension are
/// skipped because they will be overwritten by the caller.
///
private async Task DeleteOldLogosAsync(int companyId, string currentExtension)
{
foreach (var ext in AllowedExtensions)
{
if (ext == currentExtension) continue;
var oldBlobName = GetCompanyLogoPath(companyId, ext);
await _blobService.DeleteAsync(_settings.Containers.CompanyLogos, oldBlobName);
}
}
}