using GoogleApi.Configuration; using GoogleApi.Models; using Microsoft.Extensions.Options; using Google.Ads.GoogleAds; using Google.Ads.GoogleAds.Lib; using Google.Ads.GoogleAds.V22.Common; using Google.Ads.GoogleAds.V22.Enums; using Google.Ads.GoogleAds.V22.Errors; using Google.Ads.GoogleAds.V22.Resources; using Google.Ads.GoogleAds.V22.Services; namespace GoogleApi.Services; // ✅ IMPORTANT: force "Services" to mean Google.Ads.GoogleAds.Services (not GoogleApi.Services) using GAdsServices = global::Google.Ads.GoogleAds.Services; // ✅ Avoid name collision with Google.Ads.GoogleAds.V22.Resources.BiddingStrategy using ModelBiddingStrategy = GoogleApi.Models.BiddingStrategy; public sealed class GoogleAdsService { private readonly GoogleAdsConfig _config; private readonly GoogleAdsClientFactory _clientFactory; private readonly ILogger _logger; public GoogleAdsService( IOptions config, GoogleAdsClientFactory clientFactory, ILogger logger) { _config = config.Value; _clientFactory = clientFactory; _logger = logger; } public async Task ExecuteAsync(ProviderRequest request, CancellationToken ct) { var requestId = request.RequestId ?? Guid.NewGuid().ToString("N"); var operation = (request.Operation ?? string.Empty).Trim(); _logger.LogInformation( "[GoogleAds] Executing {Operation} | RequestId={RequestId} TenantId={TenantId} RealApi={RealApi}", operation, requestId, request.TenantId, _clientFactory.IsRealApiEnabled); try { var context = new GoogleAdsContext { CustomerId = GoogleAdsClientFactory.NormalizeCustomerId(request.TenantId ?? string.Empty), LoginCustomerId = request.LoginCustomerId }; var result = operation switch { "Ping" => Ping(requestId), "TestPing" => Ping(requestId), "CreateCampaign" => await CreateCampaignAsync(request, context, requestId, ct), "GetCampaign" => await GetCampaignAsync(request, context, requestId, ct), "UpdateCampaign" => await UpdateCampaignAsync(request, context, requestId, ct), "ListCampaigns" => await ListCampaignsAsync(request, context, requestId, ct), "GetCampaignStats" => GetCampaignStats(request, requestId), "GetAccountStats" => GetAccountStats(request, requestId), "ListAccessibleCustomers" => await ListAccessibleCustomersAsync(context, requestId, ct), "" => ProviderResponse.Fail(requestId, "VALIDATION", "Operation is required"), _ => ProviderResponse.Fail(requestId, "UNKNOWN_OPERATION", $"Unknown operation: {operation}") }; _logger.LogInformation( "[GoogleAds] Completed {Operation} | RequestId={RequestId} Ok={Ok}", operation, requestId, result.Ok); return result; } catch (Exception ex) { _logger.LogError(ex, "[GoogleAds] Error in {Operation} | RequestId={RequestId}", operation, requestId); return ProviderResponse.Fail(requestId, "INTERNAL_ERROR", ex.Message); } } private ProviderResponse Ping(string requestId) => ProviderResponse.Success(requestId, new { message = "GoogleApi provider is healthy", service = "GoogleApi", realApiEnabled = _clientFactory.IsRealApiEnabled, apiVersion = _config.ApiVersion, timestamp = DateTimeOffset.UtcNow }); private async Task CreateCampaignAsync( ProviderRequest request, GoogleAdsContext context, string requestId, CancellationToken ct) { var payload = request.GetPayload(); if (string.IsNullOrWhiteSpace(payload.Name)) return ProviderResponse.Fail(requestId, "VALIDATION", "Campaign name is required"); if (payload.BudgetMicros <= 0) return ProviderResponse.Fail(requestId, "VALIDATION", "BudgetMicros must be > 0"); if (_clientFactory.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.CustomerId)) return await CreateCampaignRealAsync(payload, context, requestId, ct); var externalId = $"customers/{context.CustomerId}/campaigns/{GenerateId()}"; _logger.LogInformation("[GoogleAds] EMULATED: Created campaign {CampaignName} => {CampaignId}", payload.Name, externalId); return ProviderResponse.Success(requestId, new { externalId, name = payload.Name, type = payload.Type.ToString(), status = "ENABLED", budgetMicros = payload.BudgetMicros, biddingStrategy = payload.BiddingStrategy.ToString(), createdAt = DateTimeOffset.UtcNow, emulated = true }); } private async Task CreateCampaignRealAsync( CreateCampaignPayload payload, GoogleAdsContext context, string requestId, CancellationToken ct) { try { GoogleAdsClient client = _clientFactory.CreateClient(context); // 1) Budget CampaignBudgetServiceClient budgetService = client.GetService(GAdsServices.V22.CampaignBudgetService); var budget = new CampaignBudget { Name = $"{payload.Name} Budget ({DateTime.UtcNow:yyyyMMddHHmmss})", AmountMicros = payload.BudgetMicros, DeliveryMethod = BudgetDeliveryMethodEnum.Types.BudgetDeliveryMethod.Standard, ExplicitlyShared = false }; var budgetResponse = await budgetService.MutateCampaignBudgetsAsync( new MutateCampaignBudgetsRequest { CustomerId = context.CustomerId, Operations = { new CampaignBudgetOperation { Create = budget } } }, cancellationToken: ct); var budgetResourceName = budgetResponse.Results.FirstOrDefault()?.ResourceName; if (string.IsNullOrWhiteSpace(budgetResourceName)) return ProviderResponse.Fail(requestId, "API_ERROR", "Budget create returned no resource name"); // 2) Campaign CampaignServiceClient campaignService = client.GetService(GAdsServices.V22.CampaignService); var campaign = new Campaign { Name = payload.Name, Status = CampaignStatusEnum.Types.CampaignStatus.Enabled, AdvertisingChannelType = MapChannelType(payload.Type), CampaignBudget = budgetResourceName }; // Dates must be yyyyMMdd for Google Ads API if (!string.IsNullOrWhiteSpace(payload.StartDate)) campaign.StartDate = payload.StartDate; if (!string.IsNullOrWhiteSpace(payload.EndDate)) campaign.EndDate = payload.EndDate; // ✅ Apply bidding in a way that does NOT rely on Campaign.MaximizeClicks property existing ApplyBiddingStrategySafe(campaign, payload.BiddingStrategy); var campResponse = await campaignService.MutateCampaignsAsync( new MutateCampaignsRequest { CustomerId = context.CustomerId, Operations = { new CampaignOperation { Create = campaign } } }, cancellationToken: ct); var campaignResourceName = campResponse.Results.FirstOrDefault()?.ResourceName; return ProviderResponse.Success(requestId, new { campaignResourceName, budgetResourceName, name = payload.Name, type = payload.Type.ToString(), status = "ENABLED", budgetMicros = payload.BudgetMicros, biddingStrategy = payload.BiddingStrategy.ToString(), emulated = false }); } catch (GoogleAdsException gex) { _logger.LogError(gex, "Google Ads API error creating campaign | RequestId={RequestId}", requestId); return HandleGoogleAdsException(gex, requestId); } catch (Exception ex) { _logger.LogError(ex, "Failed to create campaign via real API"); return ProviderResponse.Fail(requestId, "API_ERROR", ex.Message); } } private async Task GetCampaignAsync( ProviderRequest request, GoogleAdsContext context, string requestId, CancellationToken ct) { var payload = request.GetPayload(); if (string.IsNullOrWhiteSpace(payload.CampaignId)) return ProviderResponse.Fail(requestId, "VALIDATION", "CampaignId is required"); if (_clientFactory.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.CustomerId)) return await GetCampaignRealAsync(payload.CampaignId, context, requestId, ct); _logger.LogInformation("[GoogleAds] EMULATED: Retrieved campaign {CampaignId}", payload.CampaignId); return ProviderResponse.Success(requestId, new { externalId = payload.CampaignId, name = "Sample Campaign", type = CampaignType.Search.ToString(), status = "ENABLED", budgetMicros = 10_000_000L, // NOTE: GetCampaignPayload doesn't have BiddingStrategy — so don't reference it biddingStrategy = ModelBiddingStrategy.MaximizeClicks.ToString(), createdAt = DateTimeOffset.UtcNow.AddDays(-7), emulated = true }); } private Task GetCampaignRealAsync( string campaignId, GoogleAdsContext context, string requestId, CancellationToken ct) { try { GoogleAdsClient client = _clientFactory.CreateClient(context); GoogleAdsServiceClient googleAdsService = client.GetService(GAdsServices.V22.GoogleAdsService); var isResourceName = campaignId.Contains("/campaigns/", StringComparison.OrdinalIgnoreCase); var where = isResourceName ? $"campaign.resource_name = '{campaignId}'" : $"campaign.id = {campaignId}"; var query = $@" SELECT campaign.resource_name, campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign_budget.amount_micros FROM campaign WHERE {where} LIMIT 1"; var resp = googleAdsService.Search(new SearchGoogleAdsRequest { CustomerId = context.CustomerId, Query = query }); var row = resp.FirstOrDefault(); if (row == null) return Task.FromResult(ProviderResponse.Fail(requestId, "NOT_FOUND", "Campaign not found")); return Task.FromResult(ProviderResponse.Success(requestId, new { campaign = new { resourceName = row.Campaign.ResourceName, id = row.Campaign.Id, name = row.Campaign.Name, status = row.Campaign.Status.ToString(), channelType = row.Campaign.AdvertisingChannelType.ToString(), budgetMicros = row.CampaignBudget?.AmountMicros }, emulated = false })); } catch (GoogleAdsException gex) { _logger.LogError(gex, "Google Ads API error getting campaign | RequestId={RequestId}", requestId); return Task.FromResult(HandleGoogleAdsException(gex, requestId)); } catch (Exception ex) { _logger.LogError(ex, "Failed to get campaign via real API"); return Task.FromResult(ProviderResponse.Fail(requestId, "API_ERROR", ex.Message)); } } private Task UpdateCampaignAsync( ProviderRequest request, GoogleAdsContext context, string requestId, CancellationToken ct) { var payload = request.GetPayload(); if (string.IsNullOrWhiteSpace(payload.CampaignId)) return Task.FromResult(ProviderResponse.Fail(requestId, "VALIDATION", "CampaignId is required")); _logger.LogInformation("[GoogleAds] EMULATED: Updated campaign {CampaignId}", payload.CampaignId); return Task.FromResult(ProviderResponse.Success(requestId, new { updated = true, campaignId = payload.CampaignId, updatedAt = DateTimeOffset.UtcNow, emulated = true })); } private async Task ListCampaignsAsync( ProviderRequest request, GoogleAdsContext context, string requestId, CancellationToken ct) { if (_clientFactory.IsRealApiEnabled && !string.IsNullOrWhiteSpace(context.CustomerId)) return await ListCampaignsRealAsync(context, requestId, ct); _logger.LogInformation("[GoogleAds] EMULATED: Listed campaigns for tenant {TenantId}", request.TenantId); var campaigns = new[] { new { id = $"customers/{context.CustomerId}/campaigns/{GenerateId()}", name = "Brand Campaign", status = "Enabled", budgetMicros = 5_000_000L }, new { id = $"customers/{context.CustomerId}/campaigns/{GenerateId()}", name = "Product Campaign", status = "Enabled", budgetMicros = 10_000_000L }, new { id = $"customers/{context.CustomerId}/campaigns/{GenerateId()}", name = "Retargeting", status = "Paused", budgetMicros = 3_000_000L } }; return ProviderResponse.Success(requestId, new { campaigns, totalCount = campaigns.Length, emulated = true }); } private Task ListCampaignsRealAsync( GoogleAdsContext context, string requestId, CancellationToken ct) { try { GoogleAdsClient client = _clientFactory.CreateClient(context); GoogleAdsServiceClient googleAdsService = client.GetService(GAdsServices.V22.GoogleAdsService); var query = @" SELECT campaign.resource_name, campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign_budget.amount_micros FROM campaign ORDER BY campaign.name"; var results = new List(); var resp = googleAdsService.Search(new SearchGoogleAdsRequest { CustomerId = context.CustomerId, Query = query }); foreach (var row in resp) { ct.ThrowIfCancellationRequested(); results.Add(new { resourceName = row.Campaign.ResourceName, id = row.Campaign.Id, name = row.Campaign.Name, status = row.Campaign.Status.ToString(), channelType = row.Campaign.AdvertisingChannelType.ToString(), budgetMicros = row.CampaignBudget?.AmountMicros }); } return Task.FromResult(ProviderResponse.Success(requestId, new { campaigns = results, totalCount = results.Count, emulated = false })); } catch (GoogleAdsException gex) { _logger.LogError(gex, "Google Ads API error listing campaigns | RequestId={RequestId}", requestId); return Task.FromResult(HandleGoogleAdsException(gex, requestId)); } catch (Exception ex) { _logger.LogError(ex, "Failed to list campaigns via real API"); return Task.FromResult(ProviderResponse.Fail(requestId, "API_ERROR", ex.Message)); } } private ProviderResponse GetCampaignStats(ProviderRequest request, string requestId) { var payload = request.GetPayload(); if (string.IsNullOrWhiteSpace(payload.CampaignId)) return ProviderResponse.Fail(requestId, "VALIDATION", "CampaignId is required"); _logger.LogInformation("[GoogleAds] EMULATED: Retrieved stats for campaign {CampaignId}", payload.CampaignId); return ProviderResponse.Success(requestId, new { campaignId = payload.CampaignId, dateRange = new { start = payload.StartDate ?? "2026-01-01", end = payload.EndDate ?? "2026-01-27" }, metrics = new { impressions = 15_234L, clicks = 487L, costMicros = 2_543_000L, conversions = 23, ctr = 0.032, averageCpcMicros = 5_222L }, emulated = true }); } private ProviderResponse GetAccountStats(ProviderRequest request, string requestId) { var payload = request.GetPayload(); _logger.LogInformation("[GoogleAds] EMULATED: Retrieved account stats for tenant {TenantId}", request.TenantId); return ProviderResponse.Success(requestId, new { tenantId = request.TenantId, dateRange = new { start = payload.StartDate ?? "2026-01-01", end = payload.EndDate ?? "2026-01-27" }, metrics = new { totalCampaigns = 5, activeCampaigns = 3, totalImpressions = 152_340L, totalClicks = 4_870L, totalCostMicros = 25_430_000L, totalConversions = 234 }, emulated = true }); } private Task ListAccessibleCustomersAsync( GoogleAdsContext context, string requestId, CancellationToken ct) { if (!_clientFactory.IsRealApiEnabled) { return Task.FromResult(ProviderResponse.Success(requestId, new { customers = new[] { "1234567890", "9876543210" }, emulated = true })); } try { GoogleAdsClient client = _clientFactory.CreateClient(context); CustomerServiceClient customerService = client.GetService(GAdsServices.V22.CustomerService); var resp = customerService.ListAccessibleCustomers(new ListAccessibleCustomersRequest()); var customers = resp.ResourceNames .Select(rn => rn.Split('/').LastOrDefault() ?? rn) .ToArray(); return Task.FromResult(ProviderResponse.Success(requestId, new { customers, rawResourceNames = resp.ResourceNames.ToArray(), emulated = false })); } catch (GoogleAdsException gex) { _logger.LogError(gex, "Google Ads API error listing accessible customers | RequestId={RequestId}", requestId); return Task.FromResult(HandleGoogleAdsException(gex, requestId)); } catch (Exception ex) { _logger.LogError(ex, "Failed to list accessible customers"); return Task.FromResult(ProviderResponse.Fail(requestId, "API_ERROR", ex.Message)); } } private static string GenerateId() => Guid.NewGuid().ToString("N")[..12]; private static AdvertisingChannelTypeEnum.Types.AdvertisingChannelType MapChannelType(CampaignType type) => type switch { CampaignType.Search => AdvertisingChannelTypeEnum.Types.AdvertisingChannelType.Search, CampaignType.Display => AdvertisingChannelTypeEnum.Types.AdvertisingChannelType.Display, CampaignType.Shopping => AdvertisingChannelTypeEnum.Types.AdvertisingChannelType.Shopping, CampaignType.Video => AdvertisingChannelTypeEnum.Types.AdvertisingChannelType.Video, CampaignType.PerformanceMax => AdvertisingChannelTypeEnum.Types.AdvertisingChannelType.PerformanceMax, _ => AdvertisingChannelTypeEnum.Types.AdvertisingChannelType.Search }; // ✅ Strategy application that avoids Campaign.MaximizeClicks property dependency private static void ApplyBiddingStrategySafe(Campaign campaign, ModelBiddingStrategy strategy) { // Try to set the enum safely without compile-time dependency on the member name. // Different library/proto generations sometimes change the C# member casing. static BiddingStrategyTypeEnum.Types.BiddingStrategyType ParseBst(params string[] names) { foreach (var n in names) { if (Enum.TryParse(n, ignoreCase: true, out var v)) return v; } return BiddingStrategyTypeEnum.Types.BiddingStrategyType.Unspecified; } campaign.BiddingStrategyType = strategy switch { ModelBiddingStrategy.ManualCpc => ParseBst("ManualCpc", "MANUAL_CPC"), ModelBiddingStrategy.MaximizeClicks => ParseBst("MaximizeClicks", "MAXIMIZE_CLICKS", "MaximizeClick"), ModelBiddingStrategy.MaximizeConversions => ParseBst("MaximizeConversions", "MAXIMIZE_CONVERSIONS"), ModelBiddingStrategy.TargetCpa => ParseBst("TargetCpa", "TARGET_CPA"), ModelBiddingStrategy.TargetRoas => ParseBst("TargetRoas", "TARGET_ROAS"), _ => ParseBst("MaximizeClicks", "MAXIMIZE_CLICKS", "Unspecified") }; // Optional: set oneof objects ONLY when you know your generated Campaign has them. // ManualCpc is the most consistently present. if (strategy == ModelBiddingStrategy.ManualCpc) { campaign.ManualCpc = new ManualCpc(); } // If your Campaign class DOES have these properties in your build, you can uncomment: // if (strategy == ModelBiddingStrategy.MaximizeClicks) campaign.MaximizeClicks = new MaximizeClicks(); // if (strategy == ModelBiddingStrategy.MaximizeConversions) campaign.MaximizeConversions = new MaximizeConversions(); } private static ProviderResponse HandleGoogleAdsException(GoogleAdsException gex, string requestId) { var errorDetails = gex.Failure?.Errors?.Select(e => new { errorCode = e.ErrorCode?.ToString(), message = e.Message, trigger = e.Trigger?.StringValue, location = e.Location?.FieldPathElements?.Select(f => f.FieldName).ToArray() }).ToList(); return ProviderResponse.Fail(requestId, "GOOGLE_ADS_ERROR", gex.Message, new { googleRequestId = gex.RequestId, errors = errorDetails }); } }