using System.Collections.Concurrent; using System.Text; using MySqlConnector; namespace UtopiaCanteenSystem.Services; /// /// Fetches employee photo from hrms.employee_photo by parent_document_id (employee document id from lookup). /// Handles data URI (data:image/...;base64,...) or raw image bytes. Caches by key to avoid repeated DB calls. /// public class EmployeePhotoService : IEmployeePhotoService { private readonly IConfigService _configService; private readonly ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); private const int MaxCacheEntries = 50; public EmployeePhotoService(IConfigService configService) { _configService = configService; } /// public async Task GetPhotoBytesAsync(string parentDocumentId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(parentDocumentId)) return null; var key = parentDocumentId.Trim(); if (_cache.TryGetValue(key, out var cached)) return cached; var connectionString = _configService.GetHrmsLookupConnectionString(); if (string.IsNullOrWhiteSpace(connectionString)) return null; try { await using var conn = new MySqlConnection(connectionString); await conn.OpenAsync(cancellationToken).ConfigureAwait(false); // employee_photo.employee_id stores the employee document id (parent_document_id from employee lookup) const string sql = "SELECT photo_blob FROM hrms.employee_photo WHERE employee_id = @parentDocumentId LIMIT 1"; await using var cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@parentDocumentId", key); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) return null; if (reader.IsDBNull(0)) return null; byte[] blob; try { var len = reader.GetBytes(0, 0, null, 0, 0); blob = new byte[len]; reader.GetBytes(0, 0, blob, 0, (int)len); } catch { return null; } var imageBytes = DecodeBlobToImageBytes(blob); if (imageBytes == null || imageBytes.Length == 0) return null; // Cache (evict old if needed) while (_cache.Count >= MaxCacheEntries && _cache.Count > 0) { var first = _cache.Keys.FirstOrDefault(); if (first != null) _cache.TryRemove(first, out _); else break; } _cache[key] = imageBytes; return imageBytes; } catch { return null; } } /// /// Converts blob to image bytes: if blob is UTF-8 "data:image...;base64,<payload>", decodes base64; else treats as raw image. /// internal static byte[]? DecodeBlobToImageBytes(byte[] blob) { if (blob == null || blob.Length == 0) return null; try { var str = Encoding.UTF8.GetString(blob); if (str.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { var commaIndex = str.IndexOf(','); if (commaIndex < 0) return null; var base64 = str.Substring(commaIndex + 1).Trim(); return Convert.FromBase64String(base64); } return blob; } catch { return null; } } }