using System.Net;
using System.Net.Http.Headers;
using Metin2Launcher.Logging;

namespace Metin2Launcher.Transfer;

/// <summary>
/// Downloads a content-addressed blob over HTTP with:
///   - HTTP Range resume (picks up where the staging file left off)
///   - sha256 verification after each full download
///   - Retry with exponential backoff (capped at MaxRetries)
///
/// Blob URL layout: {baseUrl}/{hash[0:2]}/{hash}
/// </summary>
public sealed class BlobDownloader
{
    private readonly HttpClient _http;
    private readonly string _baseUrl;
    private readonly int _maxRetries;

    public BlobDownloader(HttpClient http, string baseUrl, int maxRetries)
    {
        _http = http;
        _baseUrl = baseUrl.TrimEnd('/');
        _maxRetries = maxRetries;
    }

    public static string BlobUrl(string baseUrl, string sha256)
    {
        if (sha256.Length < 2)
            throw new ArgumentException("sha256 too short", nameof(sha256));
        var prefix = sha256.Substring(0, 2);
        return $"{baseUrl.TrimEnd('/')}/{prefix}/{sha256}";
    }

    /// <summary>
    /// Downloads a blob into <paramref name="destinationPath"/>, resuming if the file already exists.
    /// Throws <see cref="BlobDownloadException"/> after all retries fail or on final hash mismatch.
    /// </summary>
    public async Task DownloadAsync(
        string sha256,
        long expectedSize,
        string destinationPath,
        CancellationToken ct,
        IProgress<long>? bytesProgress = null)
    {
        var url = BlobUrl(_baseUrl, sha256);
        Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);

        Exception? lastError = null;
        for (int attempt = 1; attempt <= _maxRetries; attempt++)
        {
            ct.ThrowIfCancellationRequested();
            try
            {
                await DownloadOnceAsync(url, expectedSize, destinationPath, ct, bytesProgress).ConfigureAwait(false);
                var actual = FileHasher.HashFileOrNull(destinationPath);
                if (actual == sha256)
                    return;

                Log.Warn($"blob hash mismatch for {sha256} (got {actual}), attempt {attempt}");
                TryDelete(destinationPath);
                lastError = new BlobDownloadException($"hash mismatch for {sha256}: got {actual}");
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (Exception ex)
            {
                Log.Warn($"blob download attempt {attempt} failed for {sha256}: {ex.Message}");
                lastError = ex;
            }

            if (attempt < _maxRetries)
            {
                var delay = TimeSpan.FromMilliseconds(200 * Math.Pow(2, attempt - 1));
                try { await Task.Delay(delay, ct).ConfigureAwait(false); }
                catch (OperationCanceledException) { throw; }
            }
        }

        throw new BlobDownloadException(
            $"blob {sha256} failed after {_maxRetries} attempts", lastError);
    }

    private async Task DownloadOnceAsync(string url, long expectedSize, string destinationPath, CancellationToken ct, IProgress<long>? bytesProgress)
    {
        long have = File.Exists(destinationPath) ? new FileInfo(destinationPath).Length : 0;
        if (have > expectedSize)
        {
            // Local file is longer than the target — nothing to resume, start over.
            TryDelete(destinationPath);
            have = 0;
        }
        if (have == expectedSize)
        {
            // Full size already on disk — the caller's hash check decides validity.
            bytesProgress?.Report(expectedSize);
            return;
        }
        if (have > 0)
            bytesProgress?.Report(have);

        using var req = new HttpRequestMessage(HttpMethod.Get, url);
        if (have > 0)
            req.Headers.Range = new RangeHeaderValue(have, null);

        using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct)
            .ConfigureAwait(false);

        if (have > 0 && resp.StatusCode != HttpStatusCode.PartialContent)
        {
            // Server ignored our Range header — restart from scratch.
            TryDelete(destinationPath);
            have = 0;
        }
        else
        {
            resp.EnsureSuccessStatusCode();
        }

        var mode = have > 0 ? FileMode.Append : FileMode.Create;
        await using var fs = new FileStream(destinationPath, mode, FileAccess.Write, FileShare.None, 1024 * 1024);
        await using var net = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);

        if (bytesProgress is null)
        {
            await net.CopyToAsync(fs, 1024 * 1024, ct).ConfigureAwait(false);
        }
        else
        {
            var buf = new byte[1024 * 1024];
            long total = have;
            int read;
            while ((read = await net.ReadAsync(buf, ct).ConfigureAwait(false)) > 0)
            {
                await fs.WriteAsync(buf.AsMemory(0, read), ct).ConfigureAwait(false);
                total += read;
                bytesProgress.Report(total);
            }
        }
    }

    private static void TryDelete(string path)
    {
        try { if (File.Exists(path)) File.Delete(path); } catch { }
    }
}

public sealed class BlobDownloadException : Exception
{
    public BlobDownloadException(string message) : base(message) { }
    public BlobDownloadException(string message, Exception? inner) : base(message, inner) { }
}
