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;
}
}
}