using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Logging;

namespace YMhut.Box.WinUI.Services;

public sealed class DownloadHostProcessService(ILogService? logService = null) : IDownloadHostProcessService
{
    private static readonly TimeSpan HostStartTimeout = TimeSpan.FromSeconds(8);

    private readonly SemaphoreSlim _writeGate = new(1, 1);
    private readonly object _sync = new();

    private Process? _process;
    private NamedPipeServerStream? _pipe;
    private StreamReader? _reader;
    private StreamWriter? _writer;
    private CancellationTokenSource? _readerCancellation;
    private bool _disposed;

    public event EventHandler<DownloadProgressSnapshot>? ProgressChanged;

    public async Task StartAsync(DownloadItem item, CancellationToken cancellationToken = default)
    {
        await SendAsync(new DownloadHostMessage(DownloadHostProtocol.Start, item.Id, Item: item), cancellationToken)
            .ConfigureAwait(false);
    }

    public async Task PauseAsync(string id, CancellationToken cancellationToken = default)
    {
        await SendAsync(new DownloadHostMessage(DownloadHostProtocol.Pause, id), cancellationToken).ConfigureAwait(false);
    }

    public async Task ResumeAsync(DownloadItem item, CancellationToken cancellationToken = default)
    {
        await SendAsync(new DownloadHostMessage(DownloadHostProtocol.Resume, item.Id, Item: item), cancellationToken)
            .ConfigureAwait(false);
    }

    public async Task CancelAsync(string id, CancellationToken cancellationToken = default)
    {
        await SendAsync(new DownloadHostMessage(DownloadHostProtocol.Cancel, id), cancellationToken).ConfigureAwait(false);
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;
        try
        {
            _writer?.WriteLine(DownloadHostProtocol.Serialize(new DownloadHostMessage(DownloadHostProtocol.Shutdown)));
            _writer?.Flush();
        }
        catch
        {
        }

        DisposeProcess(killProcess: false);
        _writeGate.Dispose();
    }

    private async Task SendAsync(DownloadHostMessage message, CancellationToken cancellationToken)
    {
        await EnsureHostAsync(cancellationToken).ConfigureAwait(false);
        await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
    }

    private async Task EnsureHostAsync(CancellationToken cancellationToken)
    {
        if (_process is { HasExited: false } && _pipe?.IsConnected == true && _writer is not null)
        {
            return;
        }

        DisposeProcess(killProcess: true);

        var executable = ResolveDownloadHostExecutable()
            ?? throw new FileNotFoundException("YMhut.Box.DownloadHost.exe was not found in the application output.");
        var pipeName = $"YMhutBoxDownloadHost-{Environment.ProcessId}-{Guid.NewGuid():N}";
        _pipe = new NamedPipeServerStream(
            pipeName,
            PipeDirection.InOut,
            1,
            PipeTransmissionMode.Byte,
            PipeOptions.Asynchronous);

        _process = Process.Start(new ProcessStartInfo
        {
            FileName = executable,
            Arguments = $"--pipe {pipeName}",
            WorkingDirectory = Path.GetDirectoryName(executable) ?? AppContext.BaseDirectory,
            UseShellExecute = false,
            CreateNoWindow = true
        }) ?? throw new InvalidOperationException("Unable to start the YMhut download host process.");

        await _pipe.WaitForConnectionAsync(cancellationToken).WaitAsync(HostStartTimeout, cancellationToken)
            .ConfigureAwait(false);
        _reader = new StreamReader(_pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true);
        _writer = new StreamWriter(_pipe, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 4096, leaveOpen: true)
        {
            AutoFlush = true
        };

        var readyLine = await _reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
        var ready = readyLine is null ? null : DownloadHostProtocol.Deserialize(readyLine);
        if (!string.Equals(ready?.Type, DownloadHostProtocol.Ready, StringComparison.Ordinal) ||
            !string.Equals(ready?.Version, DownloadHostProtocol.Version, StringComparison.Ordinal))
        {
            throw new InvalidOperationException("Download host did not complete the protocol handshake.");
        }

        _readerCancellation = new CancellationTokenSource();
        _ = Task.Run(() => ReadLoopAsync(_readerCancellation.Token));
        await WriteLogAsync("Information", "download", "Download host process started", Path.GetFileName(executable), cancellationToken)
            .ConfigureAwait(false);
    }

    private async Task ReadLoopAsync(CancellationToken cancellationToken)
    {
        try
        {
            while (_reader is not null && !cancellationToken.IsCancellationRequested)
            {
                var line = await _reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
                if (line is null)
                {
                    break;
                }

                var message = DownloadHostProtocol.Deserialize(line);
                if (message?.Progress is not null)
                {
                    ProgressChanged?.Invoke(this, message.Progress);
                }
            }
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception exception)
        {
            await WriteLogAsync("Warning", "download", "Download host read loop stopped", exception.Message, CancellationToken.None)
                .ConfigureAwait(false);
        }
    }

    private async Task WriteMessageAsync(DownloadHostMessage message, CancellationToken cancellationToken)
    {
        var writer = _writer ?? throw new InvalidOperationException("Download host pipe is not connected.");
        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();
        }
    }

    private void DisposeProcess(bool killProcess)
    {
        lock (_sync)
        {
            try
            {
                _readerCancellation?.Cancel();
                _readerCancellation?.Dispose();
                _reader?.Dispose();
                _writer?.Dispose();
                _pipe?.Dispose();
            }
            catch
            {
            }
            finally
            {
                _readerCancellation = null;
                _reader = null;
                _writer = null;
                _pipe = null;
            }

            try
            {
                if (_process is not null && !_process.HasExited && killProcess)
                {
                    _process.Kill(entireProcessTree: true);
                }
            }
            catch
            {
            }
            finally
            {
                _process?.Dispose();
                _process = null;
            }
        }
    }

    private async Task WriteLogAsync(string level, string category, string message, string? detail, CancellationToken cancellationToken)
    {
        if (logService is not null)
        {
            await logService.WriteAsync(level, category, message, detail, cancellationToken).ConfigureAwait(false);
        }
    }

    private static string? ResolveDownloadHostExecutable()
    {
        var baseDirectory = AppContext.BaseDirectory;
        var candidates = new List<string>
        {
            Path.Combine(baseDirectory, "download-host", "win-x64", "YMhut.Box.DownloadHost.exe"),
            Path.Combine(baseDirectory, "download-host", "YMhut.Box.DownloadHost.exe"),
            Path.Combine(baseDirectory, "YMhut.Box.DownloadHost.exe")
        };

        var directory = new DirectoryInfo(baseDirectory);
        while (directory is not null)
        {
            candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.DownloadHost", "bin", "Debug", "net10.0", "YMhut.Box.DownloadHost.exe"));
            candidates.Add(Path.Combine(directory.FullName, "src", "YMhut.Box.DownloadHost", "bin", "Release", "net10.0", "YMhut.Box.DownloadHost.exe"));
            directory = directory.Parent;
        }

        return candidates.FirstOrDefault(File.Exists);
    }
}
