using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using AIStudio.Settings;
namespace AIStudio.Assistants.VisualBriefing;
///
/// Defines VisualBriefingStore for the visual briefing feature.
///
public sealed partial class VisualBriefingStore(
VisualBriefingArtifactService artifactService,
ILogger logger,
VisualBriefingStorageOptions? storageOptions = null)
{
/// Defines the project manifest filename.
private const string MANIFEST_FILE_NAME = "manifest.json";
/// Defines the last-selection filename.
private const string SELECTION_FILE_NAME = "selection.json";
/// Defines the intermediate-artifact directory.
private const string ARTIFACTS_DIRECTORY_NAME = "artifacts";
/// Defines the evidence-artifact directory.
private const string EVIDENCE_ARTIFACTS_DIRECTORY_NAME = "evidence";
/// Defines the plan-artifact directory.
private const string PLAN_ARTIFACTS_DIRECTORY_NAME = "plan";
/// Defines the content-artifact directory.
private const string CONTENT_ARTIFACTS_DIRECTORY_NAME = "content";
/// Defines the presentation-artifact directory.
private const string PRESENTATION_ARTIFACTS_DIRECTORY_NAME = "presentation";
/// Defines the build-history directory.
private const string BUILDS_DIRECTORY_NAME = "builds";
/// Defines the immutable-version directory.
private const string VERSIONS_DIRECTORY_NAME = "versions";
/// Defines the persistent-transcript directory.
private const string TRANSCRIPTS_DIRECTORY_NAME = "transcripts";
/// Gets the shared persistence JSON options.
private static readonly JsonSerializerOptions JSON_OPTIONS = VisualBriefingJson.Persistence;
/// Stores per-project process locks.
private readonly ConcurrentDictionary briefingLocks = [];
///
/// Serializes store initialization.
///
private readonly SemaphoreSlim initializationLock = new(1, 1);
///
/// Serializes last-selection writes.
///
private readonly SemaphoreSlim selectionLock = new(1, 1);
/// Tracks whether initialization and reconciliation completed.
private bool initialized;
///
/// Defines RootDirectory for the visual briefing feature.
///
private string RootDirectory => Path.Combine(
storageOptions?.DataDirectory ??
SettingsManager.DataDirectory ??
throw new InvalidOperationException("The AI Studio data directory is not initialized."),
"visualBriefings");
///
/// Reads a JSON file while treating malformed persisted diagnostics as unavailable.
///
/// The JSON model type.
/// The file path.
/// The cancellation token.
/// The parsed value, or .
private static async Task ReadJsonAsync(string path, CancellationToken token)
where T : class
{
if (!File.Exists(path))
return null;
try
{
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true);
return await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
{
return null;
}
}
///
/// Writes an immutable intermediate artifact without replacing an existing file.
///
/// The artifact path.
/// The serialized artifact.
/// The cancellation token.
private static async Task WriteImmutableArtifactAsync(
string path,
string json,
CancellationToken token)
{
await WriteTextAtomicAsync(path, json, token, overwrite: false);
}
///
/// Defines WriteTextAtomicAsync for the visual briefing feature.
///
private static async Task WriteTextAtomicAsync(
string targetPath,
string content,
CancellationToken token,
bool overwrite = true)
{
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
var temporaryPath = $"{targetPath}.tmp-{Guid.NewGuid():N}";
try
{
await File.WriteAllTextAsync(temporaryPath, content, new UTF8Encoding(false), token);
await using (var stream = new FileStream(temporaryPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, 4_096, true))
await stream.FlushAsync(token);
File.Move(temporaryPath, targetPath, overwrite);
}
finally
{
TryDeleteFile(temporaryPath);
}
}
///
/// Defines TryDeleteFile for the visual briefing feature.
///
private static void TryDeleteFile(string path)
{
try
{
if (File.Exists(path))
File.Delete(path);
}
catch
{
// Startup and rollback cleanup are best effort.
}
}
///
/// Defines PathComparer for the visual briefing feature.
///
private static StringComparer PathComparer() => OperatingSystem.IsWindows()
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal;
///
/// Defines T for the visual briefing feature.
///
private static bool IsNull(T? value) => value is null;
///
/// Defines GetLock for the visual briefing feature.
///
private SemaphoreSlim GetLock(Guid briefingId) => this.briefingLocks.GetOrAdd(briefingId, _ => new(1, 1));
///
/// Defines BriefingDirectory for the visual briefing feature.
///
private string BriefingDirectory(Guid briefingId) => Path.Combine(this.RootDirectory, briefingId.ToString("D"));
///
/// Defines ManifestPath for the visual briefing feature.
///
private string ManifestPath(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), MANIFEST_FILE_NAME);
///
/// Defines SelectionPath for the visual briefing feature.
///
private string SelectionPath() => Path.Combine(this.RootDirectory, SELECTION_FILE_NAME);
///
/// Defines VersionsDirectory for the visual briefing feature.
///
private string VersionsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), VERSIONS_DIRECTORY_NAME);
///
/// Defines TranscriptsDirectory for the visual briefing feature.
///
private string TranscriptsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), TRANSCRIPTS_DIRECTORY_NAME);
///
/// Defines ArtifactsDirectory for the visual briefing feature.
///
private string ArtifactsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), ARTIFACTS_DIRECTORY_NAME);
///
/// Defines EvidenceArtifactsDirectory for the visual briefing feature.
///
private string EvidenceArtifactsDirectory(Guid briefingId) =>
Path.Combine(this.ArtifactsDirectory(briefingId), EVIDENCE_ARTIFACTS_DIRECTORY_NAME);
///
/// Defines PlanArtifactsDirectory for the visual briefing feature.
///
private string PlanArtifactsDirectory(Guid briefingId) =>
Path.Combine(this.ArtifactsDirectory(briefingId), PLAN_ARTIFACTS_DIRECTORY_NAME);
///
/// Defines ContentArtifactsDirectory for the visual briefing feature.
///
private string ContentArtifactsDirectory(Guid briefingId) =>
Path.Combine(this.ArtifactsDirectory(briefingId), CONTENT_ARTIFACTS_DIRECTORY_NAME);
///
/// Defines PresentationArtifactsDirectory for the visual briefing feature.
///
private string PresentationArtifactsDirectory(Guid briefingId) =>
Path.Combine(this.ArtifactsDirectory(briefingId), PRESENTATION_ARTIFACTS_DIRECTORY_NAME);
///
/// Defines BuildsDirectory for the visual briefing feature.
///
private string BuildsDirectory(Guid briefingId) => Path.Combine(this.BriefingDirectory(briefingId), BUILDS_DIRECTORY_NAME);
///
/// Defines EvidenceArtifactPath for the visual briefing feature.
///
private string EvidenceArtifactPath(Guid briefingId, Guid artifactId) =>
Path.Combine(this.EvidenceArtifactsDirectory(briefingId), $"{artifactId:D}.json");
///
/// Defines PlanArtifactPath for the visual briefing feature.
///
private string PlanArtifactPath(Guid briefingId, Guid artifactId) =>
Path.Combine(this.PlanArtifactsDirectory(briefingId), $"{artifactId:D}.json");
///
/// Defines ContentArtifactPath for the visual briefing feature.
///
private string ContentArtifactPath(Guid briefingId, Guid artifactId) =>
Path.Combine(this.ContentArtifactsDirectory(briefingId), $"{artifactId:D}.json");
///
/// Defines PresentationArtifactPath for the visual briefing feature.
///
private string PresentationArtifactPath(Guid briefingId, Guid artifactId) =>
Path.Combine(this.PresentationArtifactsDirectory(briefingId), $"{artifactId:D}.json");
///
/// Defines BuildPath for the visual briefing feature.
///
private string BuildPath(Guid briefingId, Guid buildId) =>
Path.Combine(this.BuildsDirectory(briefingId), $"{buildId:D}.json");
///
/// Defines TranscriptPath for the visual briefing feature.
///
private string TranscriptPath(Guid briefingId, Guid sourceId) => Path.Combine(this.TranscriptsDirectory(briefingId), $"{sourceId:D}.md");
///
/// Defines VersionPath for the visual briefing feature.
///
private string VersionPath(Guid briefingId, VisualBriefingVersion version) => Path.Combine(this.VersionsDirectory(briefingId), version.FileName);
}