using System.Diagnostics;
using System.Text;

namespace TeleWave.Infrastructure.Media;

internal sealed record ProcessResult(int ExitCode, string StdOut, string StdErr);

/// <summary>Тонкая обёртка над <see cref="Process"/> для запуска ffmpeg/ffprobe с захватом вывода
/// и понижением приоритета (чтобы транскод не мешал эфиру и API).</summary>
internal static class ProcessRunner
{
    public static async Task<ProcessResult> RunAsync(
        string fileName,
        IEnumerable<string> arguments,
        bool lowPriority,
        TimeSpan timeout,
        CancellationToken cancellationToken
    )
    {
        var psi = new ProcessStartInfo
        {
            FileName = fileName,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
        };
        foreach (var arg in arguments)
            psi.ArgumentList.Add(arg);

        using var process = new Process { StartInfo = psi };
        var stdOut = new StringBuilder();
        var stdErr = new StringBuilder();
        process.OutputDataReceived += (_, e) =>
        {
            if (e.Data is not null)
                stdOut.AppendLine(e.Data);
        };
        process.ErrorDataReceived += (_, e) =>
        {
            if (e.Data is not null)
                stdErr.AppendLine(e.Data);
        };

        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        if (lowPriority)
        {
            try
            {
                process.PriorityClass = ProcessPriorityClass.BelowNormal;
            }
            catch (Exception ex)
                when (ex is InvalidOperationException or PlatformNotSupportedException)
            {
                // Процесс мог завершиться мгновенно или платформа не поддерживает — не критично.
            }
        }

        // Таймаут (если задан) — отдельный источник отмены, слитый с внешним: зависший ffmpeg/ffprobe
        // (битый источник, -stream_loop и т.п.) не должен держать слот параллелизма/тик планировщика вечно.
        using var timeoutCts =
            timeout > TimeSpan.Zero ? new CancellationTokenSource(timeout) : null;
        using var linked = timeoutCts is null
            ? null
            : CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
        var waitToken = linked?.Token ?? cancellationToken;

        try
        {
            await process.WaitForExitAsync(waitToken);
        }
        catch (OperationCanceledException)
        {
            TryKill(process);
            if (
                timeoutCts is { IsCancellationRequested: true }
                && !cancellationToken.IsCancellationRequested
            )
                throw new TimeoutException(
                    $"Процесс {fileName} превысил таймаут {timeout.TotalSeconds:0}с и был прерван."
                );
            throw;
        }

        return new ProcessResult(process.ExitCode, stdOut.ToString(), stdErr.ToString());
    }

    private static void TryKill(Process process)
    {
        try
        {
            if (!process.HasExited)
                process.Kill(entireProcessTree: true);
        }
        catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)
        {
            // Уже завершился — игнорируем.
        }
    }
}
