using Avalonia.Controls; using Avalonia.Interactivity; using Catalog.Communication.Abstractions; using Catalog.Communication.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace ImageCatalog_2.AvaloniaViews; public partial class RaceUploadTabView : Avalonia.Controls.UserControl { private readonly ILogger _logger; public RaceUploadTabView() { InitializeComponent(); _logger = Program.ServiceProvider.GetService(typeof(ILogger)) as ILogger ?? NullLogger.Instance; } private static IRaceUploadCommunicationClient CreateClient() { return Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient ?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile."); } private async void CreateRace_Click(object? sender, RoutedEventArgs e) { var outputBox = this.FindControl("ApiOutputTextBox"); var statusBlock = this.FindControl("ApiStatusTextBlock"); var createButton = this.FindControl("ApiCreateRaceButton"); var uploadButton = this.FindControl("ApiUploadButton"); if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null) { return; } if (DataContext is not DataModel model) { statusBlock.Text = "DataContext non valido."; return; } var login = model.ApiLogin?.Trim() ?? string.Empty; var password = model.ApiPassword ?? string.Empty; var descriptionRaw = model.ApiRaceDescription?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(descriptionRaw)) { statusBlock.Text = "Inserisci login, password e descrizione gara."; return; } if (!long.TryParse(model.ApiRaceTypeId?.Trim(), out var tipoGaraId) || tipoGaraId <= 0) { statusBlock.Text = "Tipo gara non valido."; return; } createButton.IsEnabled = false; uploadButton.IsEnabled = false; statusBlock.Text = "Creazione gara in corso..."; outputBox.Text = string.Empty; try { var startDate = DateOnly.FromDateTime(model.ApiRaceStartDate.Date); var endDate = DateOnly.FromDateTime((model.ApiRaceEndDate == default ? model.ApiRaceStartDate : model.ApiRaceEndDate).Date); var sanitizedDescription = SanitizeRaceDescription(descriptionRaw); var loginResponse = await LoginAsync(login, password).ConfigureAwait(true); RawEndpointResponse saveResponse; RawEndpointResponse createPointsResponse; long raceId; var client = CreateClient(); try { saveResponse = await client.SaveRaceAsync( new RaceSaveRequest { IdGara = 0, Description = sanitizedDescription, StartDate = startDate, EndDate = endDate, TipoGaraId = tipoGaraId, EventoInLinea = model.ApiEventoInLineaIndex, TipoIndicizzazione = model.ApiTipoIndexValue, FreeEvent = model.ApiFreeEventIndex, PathBase = model.ApiPathBase?.Trim(), Localita = model.ApiLocalita?.Trim(), }, CancellationToken.None).ConfigureAwait(false); raceId = ExtractRaceId(saveResponse.Body); if (raceId <= 0) { throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio."); } model.ApiRaceId = raceId.ToString(); createPointsResponse = await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(false); } finally { (client as IDisposable)?.Dispose(); } var sb = new StringBuilder(); sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}"); sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}"); sb.AppendLine($"Crea Punti HTTP: {(int)createPointsResponse.StatusCode} {createPointsResponse.StatusCode}"); sb.AppendLine($"id_gara: {raceId}"); sb.AppendLine(); sb.AppendLine("Gara creata e avvio creazione punti richiesto."); outputBox.Text = sb.ToString(); statusBlock.Text = "Gara creata."; } catch (Exception ex) { _logger.LogError(ex, "Race creation failed in Avalonia tab."); outputBox.Text = ex.ToString(); statusBlock.Text = "Errore durante la creazione gara."; } finally { createButton.IsEnabled = true; uploadButton.IsEnabled = true; } } private async void UploadProcessed_Click(object? sender, RoutedEventArgs e) { var outputBox = this.FindControl("ApiOutputTextBox"); var statusBlock = this.FindControl("ApiStatusTextBlock"); var createButton = this.FindControl("ApiCreateRaceButton"); var uploadButton = this.FindControl("ApiUploadButton"); if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null) { return; } if (DataContext is not DataModel model) { statusBlock.Text = "DataContext non valido."; return; } var login = model.ApiLogin?.Trim() ?? string.Empty; var password = model.ApiPassword ?? string.Empty; var racePathBase = model.ApiPathBase?.Trim() ?? string.Empty; var remoteProcessedBase = model.ApiRemoteProcessedBasePath?.Trim() ?? string.Empty; if (!long.TryParse(model.ApiRaceId?.Trim(), out var raceId) || raceId <= 0) { statusBlock.Text = "id_gara non valido."; return; } if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password)) { statusBlock.Text = "Inserisci login e password."; return; } if (string.IsNullOrWhiteSpace(model.DestinationPath) || !Directory.Exists(model.DestinationPath)) { statusBlock.Text = "Cartella destinazione locale non valida."; return; } if (string.IsNullOrWhiteSpace(remoteProcessedBase)) { statusBlock.Text = "Inserisci il path base remoto per le foto processate."; return; } createButton.IsEnabled = false; uploadButton.IsEnabled = false; statusBlock.Text = "Upload foto processate in corso..."; try { await LoginAsync(login, password).ConfigureAwait(true); var files = Directory .EnumerateFiles(model.DestinationPath, "*.*", SearchOption.AllDirectories) .Where(IsSupportedImage) .ToList(); if (files.Count == 0) { statusBlock.Text = "Nessuna immagine trovata in destinazione."; outputBox.Text = "Nessun file processato da inviare."; return; } var uploaded = 0; var sb = new StringBuilder(); sb.AppendLine($"File da inviare: {files.Count}"); List pointIds; var client = CreateClient(); try { foreach (var file in files) { var relativePath = Path.GetRelativePath(model.DestinationPath, file); var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty; var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir); await using var stream = File.OpenRead(file); await client.UploadFileToReceiverAsync( new ReceiveFileUploadRequest { FileName = Path.GetFileName(file), FileStream = stream, DestinationPath = remotePath, OverwriteRemoteFile = true, }, CancellationToken.None).ConfigureAwait(true); uploaded++; if (uploaded % 20 == 0 || uploaded == files.Count) { statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}"; } } sb.AppendLine($"Upload completato: {uploaded}/{files.Count}"); statusBlock.Text = "Creazione punti foto e indicizzazione in corso..."; await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true); pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true); foreach (var pointId in pointIds) { await client.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true); } } finally { (client as IDisposable)?.Dispose(); } sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}"); outputBox.Text = sb.ToString(); statusBlock.Text = "Upload e indicizzazione completati."; } catch (Exception ex) { _logger.LogError(ex, "Upload flow failed in Avalonia tab."); outputBox.Text = ex.ToString(); statusBlock.Text = "Errore durante upload/indicizzazione."; } finally { createButton.IsEnabled = true; uploadButton.IsEnabled = true; } } private async Task LoginAsync(string login, string password) { var client = CreateClient(); try { return await client.LoginAdminAsync( new AdminLoginRequest { Login = login, Password = password, Command = "check", }, CancellationToken.None).ConfigureAwait(false); } finally { (client as IDisposable)?.Dispose(); } } private async Task> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken) { const int maxAttempts = 10; var client = CreateClient(); try { for (var attempt = 1; attempt <= maxAttempts; attempt++) { var response = await client.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false); var ids = ExtractPointIds(response.Body); if (ids.Count > 0) { return ids; } await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false); } return new List(); } finally { (client as IDisposable)?.Dispose(); } return []; } private static List ExtractPointIds(string html) { return Regex .Matches(html ?? string.Empty, @"indexFoto\((\d+)\)", RegexOptions.IgnoreCase) .Select(m => long.TryParse(m.Groups[1].Value, out var value) ? value : 0L) .Where(v => v > 0) .Distinct() .ToList(); } private static string SanitizeRaceDescription(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } var cleaned = Regex.Replace(value, "[^A-Za-z0-9 _-]", " "); return Regex.Replace(cleaned, "\\s+", " ").Trim(); } private static string CombineRemotePath(string remoteBase, string racePathBase, string relativeDir) { var segments = new[] { remoteBase, racePathBase, relativeDir } .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => s!.Replace('\\', '/').Trim('/')); var joined = string.Join('/', segments); return string.IsNullOrWhiteSpace(joined) ? "/" : joined + "/"; } private static bool IsSupportedImage(string filePath) { var extension = Path.GetExtension(filePath); if (string.IsNullOrWhiteSpace(extension)) { return false; } return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) || extension.Equals(".png", StringComparison.OrdinalIgnoreCase) || extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase) || extension.Equals(".gif", StringComparison.OrdinalIgnoreCase); } private static long ExtractRaceId(string html) { if (string.IsNullOrWhiteSpace(html)) { return 0; } var inputMatch = Regex.Match( html, "id=\\\"id_gara\\\"[^>]*value=\\\"(?\\d+)\\\"", RegexOptions.IgnoreCase); if (inputMatch.Success && long.TryParse(inputMatch.Groups["id"].Value, out var idFromInput)) { return idFromInput; } var labelMatch = Regex.Match(html, "Descrizione \\(id: (?\\d+)\\)", RegexOptions.IgnoreCase); return labelMatch.Success && long.TryParse(labelMatch.Groups["id"].Value, out var idFromLabel) ? idFromLabel : 0; } }