using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using TeamUp.Modules.OrgBoard.Domain;
using TeamUp.Modules.OrgBoard.Persistence;
namespace TeamUp.Modules.OrgBoard.Profiles;
/// Upserts a product profile by (org, key, version) — the one place product profiles are written.
internal sealed class ProductProfileWriter(OrgBoardDbContext db, TimeProvider clock)
{
///
/// When true the row must not already exist: a colliding (org, key, version) trips the unique
/// index (DbUpdateException) instead of overwriting — the install path uses this so a race can't
/// clobber an existing profile.
///
public async Task UpsertAsync(
ProductProfileManifest manifest,
string body,
Guid? organizationId,
ProfileOrigin origin,
Guid? authoredByMemberId,
bool insertOnly = false,
CancellationToken cancellationToken = default)
{
var now = clock.GetUtcNow();
var canonical = $"{manifest.Id}\n{manifest.Version}\n{manifest.Name}\n{manifest.Summary}\n{body}";
var contentHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical)));
var profile = insertOnly
? null
: await db.ProductProfiles.FirstOrDefaultAsync(
p => p.OrganizationId == organizationId && p.ProfileKey == manifest.Id && p.Version == manifest.Version,
cancellationToken);
var isNew = profile is null;
profile ??= ProductProfile.Create(manifest.Id, manifest.Version, organizationId, now);
profile.Apply(manifest, body, contentHash, origin, authoredByMemberId, now);
if (isNew)
{
db.ProductProfiles.Add(profile);
}
await db.SaveChangesAsync(cancellationToken);
return profile;
}
}