using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using YMhut.Box.Core.Downloads;

Console.OutputEncoding = Encoding.UTF8;

var pipeName = ReadPipeName(args);
if (string.IsNullOrWhiteSpace(pipeName))
{
    Console.Error.WriteLine("Missing --pipe argument.");
    return 2;
}

var running = new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.Ordinal);
var paused = new ConcurrentDictionary<string, bool>(StringComparer.Ordinal);
var canceled = new ConcurrentDictionary<string, bool>(StringComparer.Ordinal);
var writeGate = new SemaphoreSlim(1, 1);

await using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await pipe.ConnectAsync(8000).ConfigureAwait(false);
using var reader = new StreamReader(pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true);
await using var writer = new StreamWriter(pipe, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 4096, leaveOpen: true)
{
    AutoFlush = true
};

await WriteAsync(new DownloadHostMessage(DownloadHostProtocol.Ready, Version: DownloadHostProtocol.Version), CancellationToken.None)
    .ConfigureAwait(false);

while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line)
{
    var message = DownloadHostProtocol.Deserialize(line);
    if (message is null)
    {
        continue;
    }

    if (string.Equals(message.Type, DownloadHostProtocol.Shutdown, StringComparison.Ordinal))
    {
        foreach (var request in running.Values)
        {
            request.Cancel();
        }

        break;
    }

    if (string.Equals(message.Type, DownloadHostProtocol.Ping, StringComparison.Ordinal))
    {
        await WriteAsync(new DownloadHostMessage(DownloadHostProtocol.Pong, message.RequestId), CancellationToken.None)
            .ConfigureAwait(false);
        continue;
    }

    if (string.Equals(message.Type, DownloadHostProtocol.Start, StringComparison.Ordinal) && message.Item is not null)
    {
        StartDownload(message.Item, resume: FileLength(message.Item.EffectivePartialPath) > 0);
        continue;
    }

    if (string.Equals(message.Type, DownloadHostProtocol.Resume, StringComparison.Ordinal) && message.Item is not null)
    {
        StartDownload(message.Item, resume: true);
        continue;
    }

    if (string.Equals(message.Type, DownloadHostProtocol.Pause, StringComparison.Ordinal))
    {
        if (running.TryRemove(message.RequestId, out var source))
        {
            paused[message.RequestId] = true;
            source.Cancel();
            source.Dispose();
        }

        continue;
    }

    if (string.Equals(message.Type, DownloadHostProtocol.Cancel, StringComparison.Ordinal))
    {
        if (running.TryRemove(message.RequestId, out var source))
        {
            source.Cancel();
            source.Dispose();
        }

        paused.TryRemove(message.RequestId, out _);
        canceled[message.RequestId] = true;
    }
}

foreach (var request in running.Values)
{
    request.Cancel();
    request.Dispose();
}

return 0;

void StartDownload(DownloadItem item, bool resume)
{
    if (running.ContainsKey(item.Id))
    {
        return;
    }

    paused.TryRemove(item.Id, out _);
    var cancelSource = new CancellationTokenSource();
    running[item.Id] = cancelSource;

    _ = Task.Run(async () =>
    {
        try
        {
            await DownloadAsync(item, resume, cancelSource.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            if (paused.ContainsKey(item.Id))
            {
                var partialLength = FileLength(item.EffectivePartialPath);
                await WriteAsync(
                    new DownloadHostMessage(
                        DownloadHostProtocol.Paused,
                        item.Id,
                        Progress: new DownloadProgressSnapshot(item.Id, DownloadState.Paused, partialLength, item.TotalBytes, 0)),
                    CancellationToken.None).ConfigureAwait(false);
            }
            else
            {
                var wasCanceled = canceled.ContainsKey(item.Id);
                if (wasCanceled)
                {
                    TryDeleteFile(item.EffectivePartialPath);
                }

                var partialLength = wasCanceled ? 0 : FileLength(item.EffectivePartialPath);
                await WriteAsync(
                    new DownloadHostMessage(
                        DownloadHostProtocol.Canceled,
                        item.Id,
                        Progress: new DownloadProgressSnapshot(item.Id, DownloadState.Canceled, partialLength, item.TotalBytes, 0)),
                    CancellationToken.None).ConfigureAwait(false);
            }
        }
        catch (Exception exception)
        {
            await WriteAsync(
                new DownloadHostMessage(
                    DownloadHostProtocol.Failed,
                    item.Id,
                    Progress: new DownloadProgressSnapshot(item.Id, DownloadState.Failed, FileLength(item.EffectivePartialPath), item.TotalBytes, 0, exception.Message),
                    Error: exception.Message),
                CancellationToken.None).ConfigureAwait(false);
        }
        finally
        {
            paused.TryRemove(item.Id, out _);
            canceled.TryRemove(item.Id, out _);
            if (running.TryRemove(item.Id, out var source))
            {
                source.Dispose();
            }
        }
    });
}

async Task DownloadAsync(DownloadItem item, bool resume, CancellationToken cancellationToken)
{
    Directory.CreateDirectory(Path.GetDirectoryName(item.TargetPath) ?? AppContext.BaseDirectory);
    Directory.CreateDirectory(Path.GetDirectoryName(item.EffectivePartialPath) ?? AppContext.BaseDirectory);

    using var client = new HttpClient { Timeout = TimeSpan.FromHours(6) };
    client.DefaultRequestHeaders.UserAgent.ParseAdd("YMhutBox/2.0 DownloadHost");

    var allowResume = resume;
    for (var attempt = 0; attempt < 2; attempt++)
    {
        var existingBytes = allowResume ? FileLength(item.EffectivePartialPath) : 0;
        using var request = new HttpRequestMessage(HttpMethod.Get, item.Source.EffectiveUrl);
        if (existingBytes > 0)
        {
            request.Headers.Range = new RangeHeaderValue(existingBytes, null);
        }

        using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
        var metadata = ResponseMetadata.From(response);
        if (ShouldRestartFromZero(item, metadata, response.StatusCode, existingBytes))
        {
            TryDeleteFile(item.EffectivePartialPath);
            allowResume = false;
            if (attempt == 0)
            {
                continue;
            }

            existingBytes = 0;
        }

        if (existingBytes > 0 && response.StatusCode == HttpStatusCode.OK)
        {
            TryDeleteFile(item.EffectivePartialPath);
            existingBytes = 0;
        }

        response.EnsureSuccessStatusCode();
        var remoteLength = response.Content.Headers.ContentLength;
        var totalBytes = response.Content.Headers.ContentRange?.Length ??
            metadata.ContentLength ??
            (remoteLength is > 0 ? remoteLength + existingBytes : item.TotalBytes);

        await using var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        await using var target = new FileStream(
            item.EffectivePartialPath,
            existingBytes > 0 ? FileMode.Append : FileMode.Create,
            FileAccess.Write,
            FileShare.Read,
            128 * 1024,
            useAsync: true);

        var buffer = new byte[128 * 1024];
        var received = existingBytes;
        var started = Stopwatch.StartNew();
        var lastReport = Stopwatch.StartNew();
        var restartedFromZero = attempt > 0 || (resume && existingBytes == 0);

        await ReportAsync(item.Id, DownloadState.Running, received, totalBytes, 0, metadata, restartedFromZero, cancellationToken).ConfigureAwait(false);
        while (true)
        {
            var read = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false);
            if (read <= 0)
            {
                break;
            }

            await target.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
            received += read;
            if (lastReport.ElapsedMilliseconds >= 250)
            {
                var speed = (received - existingBytes) / Math.Max(0.001, started.Elapsed.TotalSeconds);
                await ReportAsync(item.Id, DownloadState.Running, received, totalBytes, speed, metadata, restartedFromZero, cancellationToken).ConfigureAwait(false);
                lastReport.Restart();
            }
        }

        await target.FlushAsync(cancellationToken).ConfigureAwait(false);
        target.Close();

        MovePartialToFinal(item.EffectivePartialPath, item.TargetPath);
        await WriteAsync(
            new DownloadHostMessage(
                DownloadHostProtocol.Completed,
                item.Id,
                Progress: new DownloadProgressSnapshot(
                    item.Id,
                    DownloadState.Completed,
                    received,
                    totalBytes,
                    0,
                    ETag: metadata.ETag,
                    LastModified: metadata.LastModified,
                    AcceptRanges: metadata.AcceptRanges,
                    ContentLength: metadata.ContentLength,
                    FinalUrl: metadata.FinalUrl,
                    RestartedFromZero: restartedFromZero)),
            cancellationToken).ConfigureAwait(false);
        return;
    }

    throw new IOException("Download restart failed.");
}

async Task ReportAsync(string id, DownloadState state, long received, long? total, double speed, ResponseMetadata metadata, bool restartedFromZero, CancellationToken cancellationToken)
{
    await WriteAsync(
        new DownloadHostMessage(
            DownloadHostProtocol.Progress,
            id,
            Progress: new DownloadProgressSnapshot(
                id,
                state,
                received,
                total,
                speed,
                ETag: metadata.ETag,
                LastModified: metadata.LastModified,
                AcceptRanges: metadata.AcceptRanges,
                ContentLength: metadata.ContentLength,
                FinalUrl: metadata.FinalUrl,
                RestartedFromZero: restartedFromZero)),
        cancellationToken).ConfigureAwait(false);
}

static bool ShouldRestartFromZero(DownloadItem item, ResponseMetadata metadata, HttpStatusCode statusCode, long existingBytes)
{
    if (existingBytes <= 0)
    {
        return false;
    }

    if (statusCode != HttpStatusCode.PartialContent)
    {
        return true;
    }

    if (!string.IsNullOrWhiteSpace(item.ETag) &&
        !string.IsNullOrWhiteSpace(metadata.ETag) &&
        !string.Equals(item.ETag, metadata.ETag, StringComparison.Ordinal))
    {
        return true;
    }

    return !string.IsNullOrWhiteSpace(item.LastModified) &&
        !string.IsNullOrWhiteSpace(metadata.LastModified) &&
        !string.Equals(item.LastModified, metadata.LastModified, StringComparison.OrdinalIgnoreCase);
}

static void MovePartialToFinal(string partialPath, string targetPath)
{
    Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? AppContext.BaseDirectory);
    if (File.Exists(targetPath))
    {
        File.Delete(targetPath);
    }

    File.Move(partialPath, targetPath);
}

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

async Task WriteAsync(DownloadHostMessage message, CancellationToken cancellationToken)
{
    await writeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
    try
    {
        await writer.WriteLineAsync(DownloadHostProtocol.Serialize(message).AsMemory(), cancellationToken).ConfigureAwait(false);
        await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
    }
    finally
    {
        writeGate.Release();
    }
}

static long FileLength(string path)
{
    try
    {
        return File.Exists(path) ? new FileInfo(path).Length : 0;
    }
    catch
    {
        return 0;
    }
}

static string? ReadPipeName(string[] args)
{
    for (var index = 0; index < args.Length; index++)
    {
        if (string.Equals(args[index], "--pipe", StringComparison.OrdinalIgnoreCase) &&
            index + 1 < args.Length)
        {
            return args[index + 1];
        }
    }

    return null;
}

sealed record ResponseMetadata(
    string ETag,
    string LastModified,
    string AcceptRanges,
    long? ContentLength,
    string FinalUrl)
{
    public bool ResumeSupported =>
        AcceptRanges.Contains("bytes", StringComparison.OrdinalIgnoreCase) ||
        !string.IsNullOrWhiteSpace(ETag) ||
        !string.IsNullOrWhiteSpace(LastModified);

    public static ResponseMetadata From(HttpResponseMessage response)
    {
        var requestUri = response.RequestMessage?.RequestUri?.ToString() ?? string.Empty;
        return new ResponseMetadata(
            response.Headers.ETag?.ToString() ?? string.Empty,
            response.Content.Headers.LastModified?.ToString() ?? response.Headers.Date?.ToString() ?? string.Empty,
            string.Join(",", response.Headers.AcceptRanges.Select(value => value.ToString())),
            response.Content.Headers.ContentRange?.Length ?? response.Content.Headers.ContentLength,
            requestUri);
    }
}
