using System.Diagnostics;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Ipc;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Supervisor;

/// <summary>
///     Production <see cref="IHostProcessLauncher"/>. Spawns <c>OtOpcUa.Driver.FOCAS.Host.exe</c>
///     with the pipe name / allowed-SID / per-spawn shared secret in the environment, waits for
///     the pipe to come up, then connects a <see cref="FocasIpcClient"/> and wraps it in an
///     <see cref="IpcFocasClient"/>. On <see cref="TerminateAsync"/> best-effort kills the
///     process and closes the IPC stream.
/// </summary>
public sealed class ProcessHostLauncher : IHostProcessLauncher
{
    private readonly ProcessHostLauncherOptions _options;
    private Process? _process;
    private FocasIpcClient? _ipc;

    public ProcessHostLauncher(ProcessHostLauncherOptions options)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }

    public bool IsProcessAlive => _process is { HasExited: false };

    public async Task<IFocasClient> LaunchAsync(CancellationToken ct)
    {
        await TerminateAsync(ct).ConfigureAwait(false);

        var secret = _options.SharedSecret ?? Guid.NewGuid().ToString("N");

        var psi = new ProcessStartInfo
        {
            FileName = _options.HostExePath,
            Arguments = _options.Arguments ?? string.Empty,
            UseShellExecute = false,
            CreateNoWindow = true,
        };
        psi.Environment["OTOPCUA_FOCAS_PIPE"] = _options.PipeName;
        psi.Environment["OTOPCUA_ALLOWED_SID"] = _options.AllowedSid;
        psi.Environment["OTOPCUA_FOCAS_SECRET"] = secret;
        psi.Environment["OTOPCUA_FOCAS_BACKEND"] = _options.Backend;

        _process = Process.Start(psi)
            ?? throw new InvalidOperationException($"Failed to start {_options.HostExePath}");

        // Poll for pipe readiness up to the configured connect timeout.
        var deadline = DateTime.UtcNow + _options.ConnectTimeout;
        while (true)
        {
            ct.ThrowIfCancellationRequested();
            if (_process.HasExited)
                throw new InvalidOperationException(
                    $"FOCAS Host exited before pipe was ready (ExitCode={_process.ExitCode}).");

            try
            {
                _ipc = await FocasIpcClient.ConnectAsync(
                    _options.PipeName, secret, TimeSpan.FromSeconds(1), ct).ConfigureAwait(false);
                break;
            }
            catch (TimeoutException)
            {
                if (DateTime.UtcNow >= deadline)
                    throw new TimeoutException(
                        $"FOCAS Host pipe {_options.PipeName} did not come up within {_options.ConnectTimeout:g}.");
                await Task.Delay(TimeSpan.FromMilliseconds(250), ct).ConfigureAwait(false);
            }
        }

        return new IpcFocasClient(_ipc, _options.Series);
    }

    public async Task TerminateAsync(CancellationToken ct)
    {
        if (_ipc is not null)
        {
            try { await _ipc.DisposeAsync().ConfigureAwait(false); }
            catch { /* best effort */ }
            _ipc = null;
        }

        if (_process is not null)
        {
            try
            {
                if (!_process.HasExited)
                {
                    _process.Kill(entireProcessTree: true);
                    await _process.WaitForExitAsync(ct).ConfigureAwait(false);
                }
            }
            catch { /* best effort */ }
            finally
            {
                _process.Dispose();
                _process = null;
            }
        }
    }
}

public sealed record ProcessHostLauncherOptions(
    string HostExePath,
    string PipeName,
    string AllowedSid)
{
    public string? SharedSecret { get; init; }
    public string? Arguments { get; init; }
    public string Backend { get; init; } = "fwlib32";
    public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(15);
    public FocasCncSeries Series { get; init; } = FocasCncSeries.Unknown;
}
