using System.Text.Json; using Creative.Models; namespace Creative.Services; /// /// Main creative service - dispatches operations to appropriate handlers. /// Stateless: returns JSON, Gateway handles persistence. /// public class CreativeService { private readonly ScraperService _scraper; private readonly CopyGeneratorService _copyGen; private readonly ImageGeneratorService _imageGen; private readonly ILogger _logger; public CreativeService( ScraperService scraper, CopyGeneratorService copyGen, ImageGeneratorService imageGen, ILogger logger) { _scraper = scraper; _copyGen = copyGen; _imageGen = imageGen; _logger = logger; } /// /// Main dispatch method - routes to appropriate operation handler. /// public async Task ExecuteAsync(CreativeRequest request, CancellationToken ct) { var requestId = request.RequestId ?? Guid.NewGuid().ToString("N"); var operation = (request.Operation ?? "").Trim(); _logger.LogInformation("[Creative] Executing {Operation} | RequestId={RequestId}", operation, requestId); try { return operation switch { "Ping" => Ping(requestId), "AnalyzeUrl" => await AnalyzeUrlAsync(request, requestId, ct), "GenerateAssets" => await GenerateAssetsAsync(request, requestId, ct), "GetImages" => await GetImagesAsync(request, requestId, ct), "CreateDraft" => await CreateDraftAsync(request, requestId, ct), _ => CreativeResponse.Fail(requestId, "UNKNOWN_OPERATION", $"Unknown operation: {operation}") }; } catch (Exception ex) { _logger.LogError(ex, "[Creative] {Operation} failed | RequestId={RequestId}", operation, requestId); return CreativeResponse.Fail(requestId, "INTERNAL_ERROR", ex.Message); } } // ============================================================ // Operations // ============================================================ /// /// Health check. /// private static CreativeResponse Ping(string requestId) { return CreativeResponse.Success(requestId, new { pong = true, timestamp = DateTimeOffset.UtcNow }); } /// /// Scrape and analyze a URL. Returns structured content. /// Payload: { "url": "https://..." } /// private async Task AnalyzeUrlAsync( CreativeRequest request, string requestId, CancellationToken ct) { var url = GetPayloadString(request, "url"); if (string.IsNullOrWhiteSpace(url)) return CreativeResponse.Fail(requestId, "MISSING_URL", "payload.url is required"); url = NormalizeUrl(url); var analysis = await _scraper.AnalyzeUrlAsync(url, ct); return CreativeResponse.Success(requestId, analysis); } /// /// Generate text assets from previously analyzed content. /// Payload: { "analysis": { ... } } (UrlAnalysis object) /// private async Task GenerateAssetsAsync( CreativeRequest request, string requestId, CancellationToken ct) { var analysisJson = GetPayloadObject(request, "analysis"); if (analysisJson == null) return CreativeResponse.Fail(requestId, "MISSING_ANALYSIS", "payload.analysis is required (pass result from AnalyzeUrl)"); var analysis = JsonSerializer.Deserialize(analysisJson.Value.GetRawText()); if (analysis == null) return CreativeResponse.Fail(requestId, "INVALID_ANALYSIS", "Could not deserialize analysis object"); var (headlines, descriptions, source) = await _copyGen.GenerateAsync(analysis, ct); return CreativeResponse.Success(requestId, new { headlines, descriptions, source }); } /// /// Get images matching previously analyzed content. /// Payload: { "analysis": { ... } } (UrlAnalysis object) /// private async Task GetImagesAsync( CreativeRequest request, string requestId, CancellationToken ct) { var analysisJson = GetPayloadObject(request, "analysis"); if (analysisJson == null) return CreativeResponse.Fail(requestId, "MISSING_ANALYSIS", "payload.analysis is required (pass result from AnalyzeUrl)"); var analysis = JsonSerializer.Deserialize(analysisJson.Value.GetRawText()); if (analysis == null) return CreativeResponse.Fail(requestId, "INVALID_ANALYSIS", "Could not deserialize analysis object"); var (images, source) = await _imageGen.GenerateAsync(analysis, ct); return CreativeResponse.Success(requestId, new { images, source }); } /// /// Full pipeline: scrape URL → generate copy → source images → return campaign draft. /// Payload: { "url": "https://..." } /// Gateway persists the returned draft to tbCreativeDraft. /// private async Task CreateDraftAsync( CreativeRequest request, string requestId, CancellationToken ct) { var url = GetPayloadString(request, "url"); if (string.IsNullOrWhiteSpace(url)) return CreativeResponse.Fail(requestId, "MISSING_URL", "payload.url is required"); url = NormalizeUrl(url); // Step 1: Scrape _logger.LogInformation("[Creative] CreateDraft step 1/3: scraping {Url}", url); var analysis = await _scraper.AnalyzeUrlAsync(url, ct); // Step 2: Generate copy _logger.LogInformation("[Creative] CreateDraft step 2/3: generating copy"); var (headlines, descriptions, copySource) = await _copyGen.GenerateAsync(analysis, ct); // Step 3: Source images _logger.LogInformation("[Creative] CreateDraft step 3/3: sourcing images"); var (images, imageSource) = await _imageGen.GenerateAsync(analysis, ct); // Assemble draft - Gateway will persist this var draftId = Guid.NewGuid().ToString("N")[..12]; var draft = new CampaignDraft { DraftId = draftId, Url = url, Analysis = analysis, Headlines = headlines, Descriptions = descriptions, Images = images, Source = copySource, ImageSource = imageSource, CreatedAt = DateTimeOffset.UtcNow }; _logger.LogInformation( "[Creative] Draft assembled | DraftId={DraftId} Headlines={H} Descriptions={D} Images={I} CopySource={CS} ImageSource={IS}", draftId, headlines.Count, descriptions.Count, images.Count, copySource, imageSource); return CreativeResponse.Success(requestId, draft); } // ============================================================ // Helpers // ============================================================ private static string? GetPayloadString(CreativeRequest request, string key) { if (request.Payload == null) return null; if (!request.Payload.TryGetValue(key, out var value)) return null; return value switch { string s => s, JsonElement je when je.ValueKind == JsonValueKind.String => je.GetString(), _ => value?.ToString() }; } private static JsonElement? GetPayloadObject(CreativeRequest request, string key) { if (request.Payload == null) return null; if (!request.Payload.TryGetValue(key, out var value)) return null; if (value is JsonElement je && je.ValueKind == JsonValueKind.Object) return je; return null; } private static string NormalizeUrl(string url) { url = url.Trim(); if (!url.StartsWith("http://") && !url.StartsWith("https://")) url = "https://" + url; return url; } }