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

namespace YMhut.Box.WinUI.Services;

public interface IPluginHostProcessService : IDisposable
{
    PluginHostStatus Status { get; }

    PluginSnapshot? CurrentSnapshot { get; }

    event EventHandler<PluginSnapshot>? SnapshotChanged;

    event EventHandler<PluginHostStatus>? StatusChanged;

    Task<PluginSnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default);

    Task<PluginSnapshot> ReloadAsync(CancellationToken cancellationToken = default);

    Task<PluginSnapshot> SetPluginEnabledAsync(string pluginId, bool enabled, CancellationToken cancellationToken = default);

    Task<PluginSnapshot> SetPermissionAsync(string pluginId, PluginPermission permission, bool granted, CancellationToken cancellationToken = default);

    Task<PluginSnapshot> SetSurfaceMountedAsync(string pluginId, string surfaceId, bool mounted, CancellationToken cancellationToken = default);

    Task<PluginBridgeResponse> BridgeCallAsync(PluginBridgeRequest request, CancellationToken cancellationToken = default);

    void ResetFailedState();

    void Stop();
}

public sealed class PluginHostProcessService(ILogService? logService = null) : IPluginHostProcessService
{
    private static readonly TimeSpan HostStartTimeout = TimeSpan.FromSeconds(12);
    private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(60);
    private readonly SemaphoreSlim _startGate = new(1, 1);
    private readonly SemaphoreSlim _writeGate = new(1, 1);
    private readonly Dictionary<string, TaskCompletionSource<PluginHostMessage>> _pending = new(StringComparer.Ordinal);
    private Process? _process;
    private NamedPipeServerStream? _pipe;
    private StreamReader? _reader;
    private StreamWriter? _writer;
    private CancellationTokenSource? _readerCancellation;
    private bool _disposed;
    private bool _stopping;

    public event EventHandler<PluginSnapshot>? SnapshotChanged;

    public event EventHandler<PluginHostStatus>? StatusChanged;

    public PluginHostStatus Status { get; private set; } = PluginHostStatus.Stopped;

    public PluginSnapshot? CurrentSnapshot { get; private set; }

    public async Task<PluginSnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default)
    {
        if (CurrentSnapshot is not null && Status == PluginHostStatus.Failed)
        {
            return CurrentSnapshot;
        }

        var response = await SendRequestAsync(new PluginHostMessage(PluginHostProtocol.GetSnapshot), cancellationToken).ConfigureAwait(false);
        return ApplySnapshot(response.Snapshot);
    }

    public async Task<PluginSnapshot> ReloadAsync(CancellationToken cancellationToken = default)
    {
        ResetFailedState();
        var response = await SendRequestAsync(new PluginHostMessage(PluginHostProtocol.Reload), cancellationToken).ConfigureAwait(false);
        return ApplySnapshot(response.Snapshot);
    }

    public async Task<PluginSnapshot> SetPluginEnabledAsync(string pluginId, bool enabled, CancellationToken cancellationToken = default)
    {
        var response = await SendRequestAsync(new PluginHostMessage(PluginHostProtocol.SetPluginEnabled, PluginId: pluginId, Enabled: enabled), cancellationToken).ConfigureAwait(false);
        return ApplySnapshot(response.Snapshot);
    }

    public async Task<PluginSnapshot> SetPermissionAsync(string pluginId, PluginPermission permission, bool granted, CancellationToken cancellationToken = default)
    {
        var response = await SendRequestAsync(new PluginHostMessage(PluginHostProtocol.SetPermission, PluginId: pluginId, Permission: permission, Granted: granted), cancellationToken).ConfigureAwait(false);
        return ApplySnapshot(response.Snapshot);
    }

    public async Task<PluginSnapshot> SetSurfaceMountedAsync(string pluginId, string surfaceId, bool mounted, CancellationToken cancellationToken = default)
    {
        var response = await SendRequestAsync(new PluginHostMessage(PluginHostProtocol.SetSurfaceMounted, PluginId: pluginId, SurfaceId: surfaceId, Mounted: mounted), cancellationToken).ConfigureAwait(false);
        return ApplySnapshot(response.Snapshot);
    }

    public async Task<PluginBridgeResponse> BridgeCallAsync(PluginBridgeRequest request, CancellationToken cancellationToken = default)
    {
        var response = await SendRequestAsync(new PluginHostMessage(PluginHostProtocol.BridgeCall, BridgeRequest: request), cancellationToken).ConfigureAwait(false);
        return response.BridgeResponse ?? new PluginBridgeResponse(false, Error: response.Error ?? "Plugin host returned no bridge response.");
    }

    public void Stop()
    {
        _stopping = true;
        SetStatus(PluginHostStatus.Stopped);
        try
        {
            if (_writer is not null)
            {
                _writer.WriteLine(PluginHostProtocol.Serialize(new PluginHostMessage(PluginHostProtocol.Shutdown)));
                _writer.Flush();
            }
        }
        catch
        {
        }

        DisposeProcess(killProcess: false);
        FailPending("Plugin host was stopped.");
        CurrentSnapshot = null;
        SetStatus(PluginHostStatus.Stopped);
    }

    public void ResetFailedState()
    {
        if (Status != PluginHostStatus.Failed)
        {
            return;
        }

        FailPending("Plugin host failed and was reset.");
        DisposeProcess(killProcess: true);
        CurrentSnapshot = null;
        _stopping = false;
        SetStatus(PluginHostStatus.Stopped);
    }

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

        _disposed = true;
        Stop();
        _startGate.Dispose();
        _writeGate.Dispose();
    }

    private async Task<PluginHostMessage> SendRequestAsync(PluginHostMessage message, CancellationToken cancellationToken)
    {
        await EnsureHostAsync(cancellationToken).ConfigureAwait(false);
        var requestId = Guid.NewGuid().ToString("N");
        var request = message with { RequestId = requestId };
        var completion = new TaskCompletionSource<PluginHostMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
        lock (_pending)
        {
            _pending[requestId] = completion;
        }

        try
        {
            await WriteMessageAsync(request, cancellationToken).ConfigureAwait(false);
            using var registration = cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken));
            return await completion.Task.WaitAsync(RequestTimeout, cancellationToken).ConfigureAwait(false);
        }
        catch (TimeoutException exception)
        {
            RemovePending(requestId);

            if (!_stopping && !_disposed)
            {
                DisposeProcess(killProcess: true);
                FailPending("Plugin host request timed out.");
                SetStatus(PluginHostStatus.Failed);
            }

            var detail = $"{PluginHostRequestLabel(message.Type)} exceeded {RequestTimeout.TotalSeconds:0} seconds.";
            await WriteLogAsync("Error", "Plugin host request timed out", detail).ConfigureAwait(false);
            throw new TimeoutException($"插件宿主请求超时：{PluginHostRequestLabel(message.Type)} 在 {RequestTimeout.TotalSeconds:0} 秒内没有响应。", exception);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            RemovePending(requestId);
            throw;
        }
        catch (Exception exception)
        {
            RemovePending(requestId);

            if (!_stopping && !_disposed)
            {
                DisposeProcess(killProcess: true);
                FailPending("Plugin host request failed.");
                SetStatus(PluginHostStatus.Failed);
            }

            await WriteLogAsync("Error", "Plugin host request failed", exception.Message).ConfigureAwait(false);
            throw;
        }
    }

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

        await _startGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_process is { HasExited: false } && _pipe?.IsConnected == true && _reader is not null && _writer is not null)
            {
                return;
            }

            DisposeProcess(killProcess: true);
            _stopping = false;
            SetStatus(PluginHostStatus.Starting);
            var executable = ResolvePluginHostExecutable()
                ?? throw new FileNotFoundException("YMhut.Box.PluginHost.exe was not found in the application output.");
            var pipeName = $"YMhutBoxPluginHost-{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 plugin 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(false), bufferSize: 4096, leaveOpen: true) { AutoFlush = true };

            var readyLine = await _reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)
                ?? throw new IOException("Plugin host pipe closed before ready.");
            var ready = PluginHostProtocol.Deserialize(readyLine)
                ?? throw new InvalidDataException("Plugin host returned an invalid ready message.");
            if (!string.Equals(ready.Type, PluginHostProtocol.Ready, StringComparison.Ordinal) ||
                !string.Equals(ready.Version, PluginHostProtocol.Version, StringComparison.Ordinal))
            {
                throw new InvalidOperationException("Plugin host did not complete the protocol handshake.");
            }

            _readerCancellation = new CancellationTokenSource();
            _ = Task.Run(() => ReadLoopAsync(_readerCancellation.Token));
            SetStatus(PluginHostStatus.Ready);
            await WriteLogAsync("Information", "Plugin host process started", Path.GetFileName(executable)).ConfigureAwait(false);
        }
        catch
        {
            SetStatus(PluginHostStatus.Failed);
            FailPending("Plugin host failed to start.");
            DisposeProcess(killProcess: true);
            throw;
        }
        finally
        {
            _startGate.Release();
        }
    }

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

                var message = PluginHostProtocol.Deserialize(line);
                if (message is null)
                {
                    continue;
                }

                if (string.Equals(message.Type, PluginHostProtocol.SnapshotChanged, StringComparison.Ordinal) && message.Snapshot is not null)
                {
                    ApplySnapshot(message.Snapshot);
                    continue;
                }

                if (!string.IsNullOrWhiteSpace(message.RequestId))
                {
                    TaskCompletionSource<PluginHostMessage>? completion;
                    lock (_pending)
                    {
                        completion = _pending.Remove(message.RequestId, out var found) ? found : null;
                    }

                    completion?.TrySetResult(message);
                }
            }
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception exception)
        {
            await WriteLogAsync("Warning", "Plugin host pipe failed", exception.Message).ConfigureAwait(false);
        }

        if (!ReferenceEquals(reader, _reader))
        {
            return;
        }

        FailPending("Plugin host pipe closed unexpectedly.");
        DisposeProcess(killProcess: true);
        SetStatus(_disposed || _stopping ? PluginHostStatus.Stopped : PluginHostStatus.Failed);
    }

    private PluginSnapshot ApplySnapshot(PluginSnapshot? snapshot)
    {
        if (snapshot is null)
        {
            throw new InvalidOperationException("Plugin host did not return a snapshot.");
        }

        CurrentSnapshot = snapshot;
        SnapshotChanged?.Invoke(this, snapshot);
        return snapshot;
    }

    private async Task WriteMessageAsync(PluginHostMessage message, CancellationToken cancellationToken)
    {
        var writer = _writer ?? throw new InvalidOperationException("Plugin host pipe is not connected.");
        await _writeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await writer.WriteLineAsync(PluginHostProtocol.Serialize(message).AsMemory(), cancellationToken).ConfigureAwait(false);
            await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _writeGate.Release();
        }
    }

    private void DisposeProcess(bool killProcess)
    {
        try
        {
            _readerCancellation?.Cancel();
            _readerCancellation?.Dispose();
            _readerCancellation = null;
            _reader?.Dispose();
            _writer?.Dispose();
            _pipe?.Dispose();
        }
        catch
        {
        }
        finally
        {
            _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 void FailPending(string error)
    {
        lock (_pending)
        {
            foreach (var pending in _pending.Values)
            {
                pending.TrySetException(new IOException(error));
            }

            _pending.Clear();
        }
    }

    private void RemovePending(string requestId)
    {
        lock (_pending)
        {
            _pending.Remove(requestId);
        }
    }

    private void SetStatus(PluginHostStatus status)
    {
        if (Status == status)
        {
            return;
        }

        Status = status;
        StatusChanged?.Invoke(this, status);
    }

    private async Task WriteLogAsync(string level, string message, string? detail)
    {
        if (logService is not null)
        {
            await logService.WriteAsync(level, "plugin-host", message, detail).ConfigureAwait(false);
        }
    }

    private static string PluginHostRequestLabel(string type)
    {
        return type switch
        {
            PluginHostProtocol.GetSnapshot => "获取插件快照",
            PluginHostProtocol.Reload => "扫描插件目录",
            PluginHostProtocol.SetPluginEnabled => "切换插件启用状态",
            PluginHostProtocol.SetPermission => "更新插件权限",
            PluginHostProtocol.SetSurfaceMounted => "更新插件挂载项",
            PluginHostProtocol.BridgeCall => "插件 Bridge 调用",
            _ => type
        };
    }

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

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