diff --git a/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadOrchestrator.cs b/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadOrchestrator.cs
new file mode 100644
index 00000000..dfc2578a
--- /dev/null
+++ b/src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadOrchestrator.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GeneralUpdate.Core.Download.Models;
+
+namespace GeneralUpdate.Core.Download.Abstractions;
+
+/// Orchestrates batch downloads with concurrency control.
+public interface IDownloadOrchestrator
+{
+ Task ExecuteAsync(
+ IReadOnlyList urls,
+ string destDir,
+ int maxConcurrency = 3,
+ IProgress? progress = null,
+ CancellationToken token = default);
+}
+
+public record DownloadReport(
+ IReadOnlyList Results,
+ long TotalBytes,
+ TimeSpan TotalDuration,
+ int SuccessCount,
+ int FailedCount
+);
diff --git a/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs
new file mode 100644
index 00000000..842b77e2
--- /dev/null
+++ b/src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using GeneralUpdate.Core.Download.Abstractions;
+using GeneralUpdate.Core.Download.Executors;
+using GeneralUpdate.Core.Download.Policy;
+using GeneralUpdate.Core.Download.Models;
+
+namespace GeneralUpdate.Core.Download.Orchestrators;
+
+///
+/// Default download orchestrator with parallel execution and concurrency limit.
+///
+public class DefaultDownloadOrchestrator : IDownloadOrchestrator
+{
+ private readonly HttpClient _httpClient;
+ private readonly IDownloadPolicy _policy;
+
+ public DefaultDownloadOrchestrator(HttpClient httpClient, IDownloadPolicy? policy = null)
+ {
+ _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ _policy = policy ?? new DefaultRetryPolicy();
+ }
+
+ public async Task ExecuteAsync(
+ IReadOnlyList urls,
+ string destDir,
+ int maxConcurrency = 3,
+ IProgress? progress = null,
+ CancellationToken token = default)
+ {
+ var sw = Stopwatch.StartNew();
+ var results = new List();
+ var sem = new SemaphoreSlim(maxConcurrency);
+ long totalBytes = 0;
+
+ var tasks = urls.Select(async (url, i) =>
+ {
+ await sem.WaitAsync(token).ConfigureAwait(false);
+ try
+ {
+ var fileName = Path.GetFileName(new Uri(url).AbsolutePath);
+ if (string.IsNullOrEmpty(fileName)) fileName = $"download_{i}";
+ var destPath = Path.Combine(destDir, fileName);
+
+ var executor = new HttpDownloadExecutor(_httpClient);
+ var r = await _policy.ExecuteAsync(ct =>
+ executor.ExecuteAsync(url, destPath, progress, ct), token)
+ .ConfigureAwait(false);
+
+ lock (results)
+ {
+ results.Add(r);
+ if (r.Success) totalBytes += r.DownloadedBytes;
+ }
+ }
+ finally { sem.Release(); }
+ });
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ sw.Stop();
+
+ return new DownloadReport(
+ results,
+ totalBytes,
+ sw.Elapsed,
+ results.Count(r => r.Success),
+ results.Count(r => !r.Success));
+ }
+}
diff --git a/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs b/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs
new file mode 100644
index 00000000..f87dc696
--- /dev/null
+++ b/src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using GeneralUpdate.Core;
+using GeneralUpdate.Core.Configuration;
+
+namespace GeneralUpdate.Core.Download.Reporting;
+
+/// Reports update lifecycle events to the server.
+public interface IUpdateReporter
+{
+ Task ReportAsync(UpdateReport report, CancellationToken token = default);
+}
+
+public enum UpdateEvent { UpdateStarted, DownloadCompleted, UpdateApplied, UpdateFailed, AppStarted }
+
+public record UpdateReport(
+ string AppName,
+ string FromVersion,
+ string? ToVersion,
+ UpdateEvent Event,
+ int AppType,
+ DateTimeOffset Timestamp,
+ string? ErrorMessage = null,
+ double? DurationMs = null
+);
+
+/// HTTP POST reporter with optional HMAC signing.
+public class HttpUpdateReporter : IUpdateReporter
+{
+ private readonly HttpClient _client;
+ private readonly string _reportUrl;
+ private readonly string? _secretKey;
+
+ public HttpUpdateReporter(HttpClient client, string reportUrl, string? secretKey = null)
+ {
+ _client = client;
+ _reportUrl = reportUrl;
+ _secretKey = secretKey;
+ }
+
+ public async Task ReportAsync(UpdateReport report, CancellationToken token = default)
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(report);
+
+ using var request = new HttpRequestMessage(HttpMethod.Post, _reportUrl);
+ request.Content = new StringContent(json, Encoding.UTF8, "application/json");
+
+ if (!string.IsNullOrEmpty(_secretKey))
+ {
+ var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
+ var sig = ComputeHmac($"{json}|{ts}", _secretKey);
+ request.Headers.Add("X-Update-Timestamp", ts);
+ request.Headers.Add("X-Update-Signature", sig);
+ }
+
+ await _client.SendAsync(request, token).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ // Silent failure — reporting should never break the update flow
+ GeneralTracer.Warn($"Report failed: {ex.Message}");
+ }
+ }
+
+ private static string ComputeHmac(string data, string key)
+ {
+ var h = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(key))
+ .ComputeHash(Encoding.UTF8.GetBytes(data));
+ return BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
+ }
+}
+
+/// No-op reporter used when ReportUrl is not configured.
+public class NoOpUpdateReporter : IUpdateReporter
+{
+ public Task ReportAsync(UpdateReport report, CancellationToken token = default)
+ => Task.CompletedTask;
+}