using System.Text;
using System.Text.RegularExpressions;
using FozFc.Api.Infrastructure.Zerozero;
namespace FozFc.Api.Application.Zerozero;
///
/// Image URL helpers ported from the ZeroZero worker: normalises zerozero.pt /
/// staticzz CDN images to higher resolution and rewrites images that must be
/// proxied through this backend (/api/media/r/<base64url>).
///
public static class ZerozeroImages
{
public static string? ToAbsoluteUrl(string? url)
{
if (string.IsNullOrWhiteSpace(url)) return null;
if (url.StartsWith("http://") || url.StartsWith("https://")) return url;
if (url.StartsWith("//")) return $"https:{url}";
return $"{ZerozeroSite.Origin}{(url.StartsWith("/") ? "" : "/")}{url}";
}
public static string UpgradeLogoUrl(string url)
{
foreach (var re in new[] { @"/logos/equipas/(.+)$", @"/img/logos/equipas/(.+)$" })
{
var m = Regex.Match(url, re, RegexOptions.IgnoreCase);
if (m.Success) return $"https://cdn-img.staticzz.com/img/logos/equipas/{m.Groups[1].Value}";
}
return url;
}
public static string UpgradeNewsImageToStaticzzCdn(string url)
{
try
{
var u = new Uri(url);
var host = u.Host.ToLowerInvariant();
if (host != "zerozero.pt" && !host.EndsWith(".zerozero.pt")) return url;
var m = Regex.Match(u.AbsolutePath, @"/img/(news|noticias|galerias)/(.+)$", RegexOptions.IgnoreCase);
if (!m.Success) return url;
return $"https://cdn-img.staticzz.com/img/{m.Groups[1].Value.ToLowerInvariant()}/{m.Groups[2].Value}";
}
catch { return url; }
}
private const int NoticiasImgTargetMin = 620;
public static string NormalizeNoticiasHeroImageUrl(string url)
{
try
{
var u = new Uri(url);
var m = Regex.Match(u.AbsolutePath,
@"^(/img/(?:noticias|news)/\d+/)imgS(\d+)(I\d+T[^/]+\.(?:png|jpe?g|webp))$", RegexOptions.IgnoreCase);
if (!m.Success) return url;
var w = int.TryParse(m.Groups[2].Value, out var parsed) ? parsed : 0;
var targetW = w < NoticiasImgTargetMin ? NoticiasImgTargetMin : w;
return $"https://cdn-img.staticzz.com{m.Groups[1].Value}imgS{targetW}{m.Groups[3].Value}";
}
catch { return url; }
}
private static bool LooksLikeNewsThumbUrl(string url) =>
Regex.IsMatch(url, @"_(thumb|thumbs|t|sm|mini)(\.|$|/)", RegexOptions.IgnoreCase) ||
Regex.IsMatch(url, @"/thumb|thumbnails?|/mini|/small/", RegexOptions.IgnoreCase) ||
Regex.IsMatch(url, @"[?&]w=(?:[1-9]|[1-9]\d|1\d{2}|2\d{2}|300)\b", RegexOptions.IgnoreCase) ||
Regex.IsMatch(url, @"[?&]h=(?:[1-9]|[1-9]\d|1\d{2}|2\d{2}|300)\b", RegexOptions.IgnoreCase);
private static double ScoreNewsHeroCandidate(string url)
{
double s = 0;
var lower = url.ToLowerInvariant();
if (LooksLikeNewsThumbUrl(url)) s -= 80;
if (lower.Contains("cdn-img.staticzz") || lower.Contains("cdn-img.zerozero")) s += 25;
if (Regex.IsMatch(url, @"\b(1200|1000|960|900|800|720|640)\b")) s += 20;
var imgS = Regex.Match(url, @"imgS(\d+)", RegexOptions.IgnoreCase);
if (imgS.Success && int.TryParse(imgS.Groups[1].Value, out var ww)) s += Math.Min(ww / 8.0, 45);
s += Math.Min(url.Length, 320) * 0.04;
return s;
}
public static string? PickNewsHeroImage(IEnumerable candidates)
{
var urls = candidates
.Select(c => c?.Trim())
.Where(c => !string.IsNullOrEmpty(c))
.Select(c => ToAbsoluteUrl(c) ?? c!)
.Where(c => !string.IsNullOrEmpty(c))
.ToList();
var seen = new HashSet();
var unique = new List();
foreach (var u in urls)
{
var norm = u!.Split('?')[0].ToLowerInvariant();
if (seen.Add(norm)) unique.Add(u);
}
if (unique.Count == 0) return null;
return unique.OrderByDescending(ScoreNewsHeroCandidate).First();
}
public static string? TryUpscaleNewsMediaUrl(string? url)
{
if (string.IsNullOrEmpty(url)) return url;
try
{
var u = new Uri(url);
var host = u.Host.ToLowerInvariant();
var isZz = host is "zerozero.pt" or "cdn-img.staticzz.com" or "cdn-img.zerozero.pt" || host.EndsWith(".zerozero.pt");
if (!isZz) return url;
var changed = false;
var result = Regex.Replace(url, @"([?&]w=)(\d+)", m =>
{
var n = int.Parse(m.Groups[2].Value);
if (n > 0 && n < 900) { changed = true; return m.Groups[1].Value + "1200"; }
return m.Value;
}, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"([?&]h=)(\d+)", m =>
{
var n = int.Parse(m.Groups[2].Value);
if (n > 0 && n < 900) { changed = true; return m.Groups[1].Value + "800"; }
return m.Value;
}, RegexOptions.IgnoreCase);
if (changed) return result;
var sm = Regex.Match(u.AbsolutePath, @"^(.+)_s(\.(jpe?g|png|webp))$", RegexOptions.IgnoreCase);
if (sm.Success) return url.Replace(u.AbsolutePath, sm.Groups[1].Value + sm.Groups[2].Value);
var md = Regex.Match(u.AbsolutePath, @"^(.+)_m(\.(jpe?g|png|webp))$", RegexOptions.IgnoreCase);
if (md.Success) return url.Replace(u.AbsolutePath, md.Groups[1].Value + md.Groups[2].Value);
}
catch { return url; }
return url;
}
public static string? PickLargestFromSrcset(string? srcset)
{
if (string.IsNullOrWhiteSpace(srcset)) return null;
string? best = null;
var bestW = 0;
foreach (var raw in srcset.Split(','))
{
var p = raw.Trim();
if (p.Length == 0) continue;
var wMatch = Regex.Match(p, @"\s+(\d+)w\s*$", RegexOptions.IgnoreCase);
var xMatch = Regex.Match(p, @"\s+([\d.]+)x\s*$", RegexOptions.IgnoreCase);
int w;
string urlPart;
if (wMatch.Success) { w = int.Parse(wMatch.Groups[1].Value); urlPart = p[..^wMatch.Value.Length].Trim(); }
else if (xMatch.Success) { w = (int)Math.Round(double.Parse(xMatch.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture) * 960); urlPart = p[..^xMatch.Value.Length].Trim(); }
else { urlPart = p.Split(' ', '\t')[0]; w = 1; }
var url = urlPart.Trim('"', '\'');
if (string.IsNullOrEmpty(url)) continue;
string? abs = url.StartsWith("//") ? $"https:{url}"
: Regex.IsMatch(url, @"^https?://", RegexOptions.IgnoreCase) ? url
: url.StartsWith("/") ? $"{ZerozeroSite.Origin}{url}" : null;
if (abs is null) continue;
if (w >= bestW) { bestW = w; best = abs; }
}
return best;
}
///
/// Rewrites an image URL: high-res CDN when possible, otherwise proxied through
/// this backend at {origin}/api/media/r/<base64url>.
///
public static string? RewriteImageUrl(string? url, string origin)
{
if (string.IsNullOrEmpty(url)) return url;
try
{
var parsed = new Uri(url);
var host = parsed.Host.ToLowerInvariant();
if (host is "cdn-img.staticzz.com" or "cdn-img.zerozero.pt")
return NormalizeNoticiasHeroImageUrl(url);
if (host == "zerozero.pt" || host.EndsWith(".zerozero.pt"))
{
var upgradedLogo = UpgradeLogoUrl(url);
if (upgradedLogo != url) return NormalizeNoticiasHeroImageUrl(upgradedLogo);
var upgradedNews = UpgradeNewsImageToStaticzzCdn(url);
if (upgradedNews != url) return NormalizeNoticiasHeroImageUrl(upgradedNews);
var encoded = Base64UrlEncode(url);
return $"{origin.TrimEnd('/')}/api/media/r/{encoded}";
}
}
catch { /* keep as-is */ }
return NormalizeNoticiasHeroImageUrl(url);
}
public static string Base64UrlEncode(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
return Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
public static string Base64UrlDecode(string value)
{
var s = value.Replace('-', '+').Replace('_', '/');
switch (s.Length % 4) { case 2: s += "=="; break; case 3: s += "="; break; }
return Encoding.UTF8.GetString(Convert.FromBase64String(s));
}
}