using System.Text.Json.Serialization; namespace PlotLine.Models; public static class IllustrationLibraryCategories { public const string Character = "Character"; public const string Location = "Location"; public const string Asset = "Asset"; public static readonly IReadOnlyList All = [Character, Location, Asset]; public static bool IsValid(string? category) => All.Any(item => string.Equals(item, category, StringComparison.OrdinalIgnoreCase)); public static string StorageFolder(string category) => category switch { Character => "characters", Location => "locations", Asset => "assets", _ => throw new ArgumentOutOfRangeException(nameof(category), category, "Unknown illustration category.") }; } public static class IllustrationLibraryStatuses { public const string Planned = "Planned"; public const string Queued = "Queued"; public const string Generating = "Generating"; public const string Generated = "Generated"; public const string Approved = "Approved"; public const string Rejected = "Rejected"; public const string Failed = "Failed"; public const string Superseded = "Superseded"; public const string Disabled = "Disabled"; public static readonly IReadOnlySet All = new HashSet(StringComparer.OrdinalIgnoreCase) { Planned, Queued, Generating, Generated, Approved, Rejected, Failed, Superseded, Disabled }; public static bool IsValid(string? status) => !string.IsNullOrWhiteSpace(status) && All.Contains(status); public static bool CanTransition(string from, string to) => (from, to) switch { (Planned, Queued) => true, (Queued, Generating) => true, (Generating, Generated) => true, (Generating, Failed) => true, (Generated, Approved) => true, (Generated, Rejected) => true, (Approved, Superseded) => true, (Rejected, Queued) => true, (Failed, Queued) => true, (Approved, Disabled) => true, (Disabled, Queued) => true, _ when string.Equals(from, to, StringComparison.OrdinalIgnoreCase) => true, _ => false }; } public sealed class IllustrationGenerationSpecification { public string Category { get; init; } = string.Empty; public string Code { get; init; } = string.Empty; public string Title { get; init; } = string.Empty; public string Description { get; init; } = string.Empty; public string VisualSubject { get; init; } = string.Empty; public string Composition { get; init; } = string.Empty; public string Mood { get; init; } = string.Empty; public string? ApparentAgeBand { get; init; } public string? Presentation { get; init; } public string? HairColour { get; init; } public string? HairLength { get; init; } public string? SkinTone { get; init; } public string? ClothingEra { get; init; } public string? ClothingStyle { get; init; } public string? LocationType { get; init; } public string? Era { get; init; } public string? Setting { get; init; } public string? ObjectType { get; init; } public string? Colour { get; init; } public IReadOnlyList Features { get; init; } = []; public IReadOnlyList Palette { get; init; } = []; public IReadOnlyList MetadataTags { get; init; } = []; public Dictionary Metadata { get; init; } = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyList Validate() { var errors = new List(); if (!IllustrationLibraryCategories.IsValid(Category)) { errors.Add("Category must be Character, Location or Asset."); } if (string.IsNullOrWhiteSpace(Code)) { errors.Add("Stable code is required."); } if (string.IsNullOrWhiteSpace(Title)) { errors.Add("Title is required."); } if (string.IsNullOrWhiteSpace(Mood)) { errors.Add("Mood is required."); } if (Category == IllustrationLibraryCategories.Character) { Require(ApparentAgeBand, "Apparent age band is required for characters."); Require(Presentation, "Presentation is required for characters."); } else if (Category == IllustrationLibraryCategories.Location) { Require(LocationType, "Location type is required for locations."); } else if (Category == IllustrationLibraryCategories.Asset) { Require(ObjectType, "Object type is required for assets."); } return errors; void Require(string? value, string message) { if (string.IsNullOrWhiteSpace(value)) { errors.Add(message); } } } } public sealed class IllustrationLibraryItem { public int IllustrationLibraryItemID { get; set; } public string Category { get; set; } = string.Empty; public string StableCode { get; set; } = string.Empty; public string DisplayTitle { get; set; } = string.Empty; public string? ShortDescription { get; set; } public string? MetadataJson { get; set; } public string? SpecificationJson { get; set; } public string? FilePath { get; set; } public string? ThumbnailPath { get; set; } public int? Width { get; set; } public int? Height { get; set; } public long? FileSizeBytes { get; set; } public string? MimeType { get; set; } public string Status { get; set; } = IllustrationLibraryStatuses.Planned; public bool IsActive { get; set; } public bool IsApproved { get; set; } public DateTime CreatedUtc { get; set; } public DateTime UpdatedUtc { get; set; } public DateTime? ApprovedUtc { get; set; } public int? ApprovedByUserID { get; set; } public string? Provider { get; set; } public string? Model { get; set; } public string? PromptText { get; set; } public string? Prompt { get; set; } public string? PromptTemplateVersion { get; set; } public string? ProviderRequestID { get; set; } public int AttemptCount { get; set; } public int GenerationAttemptCount { get; set; } public int? ParentItemID { get; set; } public string? RejectionReason { get; set; } public DateTime? RejectedUtc { get; set; } public int? RejectedByUserID { get; set; } public string? AdminNotes { get; set; } public int UsageCount { get; set; } public string? LastError { get; set; } public string? ErrorMessage { get; set; } public DateTime? QueuedUtc { get; set; } public DateTime? StartedUtc { get; set; } public DateTime? CompletedUtc { get; set; } } public sealed class IllustrationLibrarySummary { public int TotalCount { get; set; } public int PlannedCount { get; set; } public int QueuedCount { get; set; } public int GeneratingCount { get; set; } public int GeneratedCount { get; set; } public int ApprovedCount { get; set; } public int RejectedCount { get; set; } public int FailedCount { get; set; } public int SupersededCount { get; set; } public int DisabledCount { get; set; } } public sealed class IllustrationLibraryFilter { public string? Category { get; init; } public string? Status { get; init; } public string? Search { get; init; } } public sealed record IllustrationPromptBuildResult( string Prompt, string TemplateVersion, string SpecificationJson, string MetadataJson); public sealed record IllustrationImageGenerationRequest( int IllustrationLibraryItemID, string StableCode, string Category, string Prompt, string TemplateVersion); public sealed record IllustrationImageGenerationResult( bool Succeeded, byte[]? ImageBytes = null, string? MimeType = null, string? Provider = null, string? Model = null, string? ProviderRequestID = null, string? ErrorMessage = null) { [JsonIgnore] public bool HasImage => ImageBytes is { Length: > 0 }; } public sealed record IllustrationStoredImage( string FilePath, string ThumbnailPath, int Width, int Height, long FileSizeBytes, string MimeType); public sealed record IllustrationLibraryActionResult(bool Succeeded, string Message); public sealed record IllustrationBulkRetryResult( int QueuedCount, int SkippedCount, int RetryLimitReachedCount) { public string Message => $"{QueuedCount:N0} failed illustration(s) queued. {SkippedCount:N0} skipped. {RetryLimitReachedCount:N0} reached the retry limit."; } public sealed record IllustrationStarterGenerationPlan( int CharactersToGenerate, int LocationsToGenerate, int AssetsToGenerate) { public int TotalToGenerate => CharactersToGenerate + LocationsToGenerate + AssetsToGenerate; }