using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.ECM; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using GB5Shared.Storage; using Microsoft.Extensions.Logging; namespace GB5Shared.Attachment { public interface IAttachmentPathResolutionService { /// /// Phase 2: moves deferred attachment files to their final paths /// and updates TATTACHMENT rows. Called after entity save once /// OBJECTID is available via SP_RESOLVE_ATTACHMENTS_BATCH. /// Task ResolvePhase2Async( int objectHeaderTypeId, Guid headerRowGuid, LoginDTO login, DbTransaction tx, CancellationToken ct = default); } public sealed class AttachmentPathResolutionService : IAttachmentPathResolutionService { private readonly IQueryExecutor _qe; private readonly ITemplateResolutionEngine _engine; private readonly IAttachmentStorageResolver _storageResolver; private readonly ILogger _log; public AttachmentPathResolutionService( IQueryExecutor qe, ITemplateResolutionEngine engine, IAttachmentStorageResolver storageResolver, ILogger log) { _qe = qe; _engine = engine; _storageResolver = storageResolver; _log = log; } public async Task ResolvePhase2Async( int objectHeaderTypeId, Guid headerRowGuid, LoginDTO login, DbTransaction tx, CancellationToken ct = default) { // ── 1. Load deferred attachments ───────────────────── var deferred = await _qe.QueryAsync( login, "EXEC DBO.SP_ATTACHMENT_GET_DEFERRED @ObjectHeaderTypeId, @HeaderRowGuid, @ClientId", new { objectHeaderTypeId, HeaderRowGuid = headerRowGuid, login.ClientId }, tx, cancellationToken: ct).ConfigureAwait(false); if (deferred is null) return; var list = new List(deferred); if (list.Count == 0) return; // ── 2. Compute new paths ────────────────────────────── var movements = await _engine .ResolvePhase2Async(list, login, ct) .ConfigureAwait(false); // Each attachment carries the backend it was actually stored under // (StorageAdapterType) — a tenant may have switched providers since upload, // so the move must target the file's own backend, not today's tenant setting. var storageOptionByAttachmentId = list.ToDictionary(d => d.AttachmentId, d => d.StorageAdapterType); // ── 3. Move files in storage + collect successful moves var resolved = new List(movements.Count); foreach (var m in movements) { try { var storage = await _storageResolver .ResolveAsync(login, storageOptionByAttachmentId[m.AttachmentId], ct) .ConfigureAwait(false); var moved = await storage .MoveAsync(m.OldCmsId, m.NewCmsId, ct) .ConfigureAwait(false); if (!moved) { _log.LogWarning( "Phase 2 storage move skipped — source not found. " + "AttachmentId={Id} OldKey={Old}", m.AttachmentId, m.OldCmsId); } resolved.Add(m); } catch (Exception ex) { _log.LogError(ex, "Phase 2 storage move failed. AttachmentId={Id} OldKey={Old} NewKey={New}", m.AttachmentId, m.OldCmsId, m.NewCmsId); // continue to next file — partial progress is still progress } } if (resolved.Count == 0) return; // ── 4. Bulk-update TATTACHMENT paths via TVP ────────── var tvp = BuildResolutionTvp(resolved); await _qe.ExecuteAsync( login, "EXEC DBO.SP_ATTACHMENT_UPDATE_RESOLVED_PATH @Resolutions", new { Resolutions = tvp }, tx, cancellationToken: ct).ConfigureAwait(false); _log.LogInformation( "Phase 2 path resolution complete. Moved={Count} of {Total}", resolved.Count, list.Count); } private static DataTable BuildResolutionTvp(IEnumerable movements) { var dt = new DataTable(); dt.Columns.Add("AttachmentId", typeof(int)); dt.Columns.Add("NewFolderName", typeof(string)); dt.Columns.Add("NewAttachedFileName", typeof(string)); dt.Columns.Add("NewCmsId", typeof(string)); dt.Columns.Add("NewDisplayFileName", typeof(string)); foreach (var m in movements) dt.Rows.Add(m.AttachmentId, m.NewFolderName, m.NewAttachedFileName, m.NewCmsId, m.NewDisplayFileName); return dt; } } }