using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace ManagerService.Helpers
{
///
/// Sonde le poids d'un blob par requête HEAD sur son URL publique, par lots.
///
/// Deux appelants : MigrationController (écart e de la bascule) et le backfill
/// des lignes existantes. Comme , c'est un calculateur
/// unique plutôt que deux implémentations qui divergeraient — ici sur la taille de lot,
/// le délai d'attente et surtout le sort réservé aux échecs.
///
/// ⚠️ Le serveur n'a aucun client de bucket : ni Firebase Admin ni Google.Cloud.Storage
/// ne sont référencés dans le projet, les blobs étant téléversés depuis manager-app
/// directement. L'URL publique est donc le seul moyen de connaître une taille.
///
public static class ResourceSizeProbe
{
/// Trente requêtes en vol à la fois, valeur héritée de la migration.
public const int BatchSize = 30;
public sealed class Result
{
private readonly ConcurrentDictionary _sizes = new();
private readonly ConcurrentDictionary _unsized = new();
public bool TryGetSize(string id, out long size) => _sizes.TryGetValue(id, out size);
///
/// Vrai si la sonde n'a pas su donner de taille : HEAD en échec, ou réponse
/// sans Content-Length. Les deux cas laissent la ressource à 0 octet,
/// donc invisible au quota — l'appelant doit le signaler, pas l'avaler.
///
public bool IsUnsized(string id) => _unsized.ContainsKey(id);
public int SizedCount => _sizes.Count;
public int UnsizedCount => _unsized.Count;
internal void Record(string id, long size) => _sizes[id] = size;
internal void RecordUnsized(string id) => _unsized[id] = true;
}
///
/// Le est fourni par l'appelant, qui reste maître du
/// délai d'attente : la migration accepte 10 s par ressource, un backfill lancé
/// à la main peut vouloir plus.
///
public static async Task ProbeAsync(HttpClient client, IEnumerable<(string Id, string Url)> targets)
{
var result = new Result();
var list = targets.Where(t => !string.IsNullOrEmpty(t.Url)).ToList();
for (int i = 0; i < list.Count; i += BatchSize)
{
var batch = list.Skip(i).Take(BatchSize);
await Task.WhenAll(batch.Select(async target =>
{
try
{
var request = new HttpRequestMessage(HttpMethod.Head, target.Url);
var response = await client.SendAsync(request);
// Un 404 ne lève pas : il répond simplement sans Content-Length.
// Le traiter comme un échec est le seul moyen de ne pas confondre
// « blob absent » et « blob de 0 octet ».
if (response.Content.Headers.ContentLength.HasValue)
result.Record(target.Id, response.Content.Headers.ContentLength.Value);
else
result.RecordUnsized(target.Id);
}
catch
{
result.RecordUnsized(target.Id);
}
}));
}
return result;
}
}
}