using System.Diagnostics;
using System.IO;

namespace vcecht_exp_helper;

public static class FfmpegOpusConverter
{
    /// <summary>
    /// Asynchronously converts a WAV (or any PCM) stream into an Opus stream.
    /// </summary>
    /// <param name="inputStream">
    ///   Any Stream (e.g. FileStream, MemoryStream, NetworkStream) containing raw audio 
    ///   FFmpeg can decode (here assumed WAV/PCM).
    /// </param>
    /// <param name="cancellationToken">
    ///   CancellationToken to abort the operation mid‐stream.
    /// </param>
    /// <returns>
    ///   A MemoryStream containing the Opus output.  The returned stream’s Position is set to 0.
    /// </returns>
    public static async Task<Stream> ConvertWavToOpusAsync(
        Stream inputStream,
        CancellationToken cancellationToken = default)
    {
        if (inputStream == null) throw new ArgumentNullException(nameof(inputStream));

        // Build ffmpeg arguments (quiet, read stdin, write stdout in Opus)
        string ffmpegArgs = string.Join(" ", new[]
        {
            "-hide_banner",
            "-loglevel", "error",
            "-nostdin",
            "-i", "pipe:0",
            "-c:a", "libopus",
            "-b:a", "16k",
            "-compression_level", "10",
            "-vbr", "on",
            "-application", "voip",
            "-frame_size", "20",
            "-deadline", "realtime",
            "-ar", "24000",
            "-f", "opus",
            "-fflags"," +genpts",
            "pipe:1"
        });

        var psi = new ProcessStartInfo
        {
            FileName            = "ffmpeg",
            Arguments           = ffmpegArgs,
            UseShellExecute     = false,
            RedirectStandardInput  = true,
            RedirectStandardOutput = true,
            RedirectStandardError  = true,
            CreateNoWindow      = true
        };

        var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };

        try
        {
            if (!ffmpeg.Start())
                throw new InvalidOperationException("Failed to start FFmpeg process.");

            //
            // 1) Write inputStream → ffmpeg.StandardInput.BaseStream
            //
            Task writeStdin = Task.Run(async () =>
            {
                try
                {
                    Stream stdinPipe = ffmpeg.StandardInput.BaseStream;
                    await inputStream.CopyToAsync(stdinPipe, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
                    // Signal EOF to ffmpeg
                    ffmpeg.StandardInput.Close();
                }
                catch (Exception ex) when (!(ex is OperationCanceledException))
                {
                    // If writing fails, kill the process so it doesn’t hang
                    try { ffmpeg.Kill(); } catch { /* ignore */ }
                    throw;
                }
            }, cancellationToken);

            //
            // 2) Read ffmpeg.StandardError (to avoid deadlocks and capture any error text)
            //
            Task<string> readStderr = Task.Run(async () =>
            {
                using (var sr = ffmpeg.StandardError)
                {
                    return await sr.ReadToEndAsync().ConfigureAwait(false);
                }
            }, cancellationToken);

            //
            // 3) Read ffmpeg.StandardOutput → MemoryStream
            //
            var outputMem = new MemoryStream();
            Task readStdout = Task.Run(async () =>
            {
                try
                {
                    Stream stdoutPipe = ffmpeg.StandardOutput.BaseStream;
                    await stdoutPipe.CopyToAsync(outputMem, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
                }
                catch (Exception ex) when (!(ex is OperationCanceledException))
                {
                    try { ffmpeg.Kill(); } catch { /* ignore */ }
                    throw;
                }
            }, cancellationToken);

            // Wait for both copying tasks to finish
            await Task.WhenAll(writeStdin, readStdout).ConfigureAwait(false);

            // Now wait for ffmpeg to exit
            await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);

            if (ffmpeg.ExitCode != 0)
            {
                string stderrText = await readStderr.ConfigureAwait(false);
                throw new InvalidOperationException(
                    $"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
            }

            // Rewind and return
            outputMem.Position = 0;
            return outputMem;
        }
        finally
        {
            ffmpeg.Dispose();
        }
    }

    /// <summary>
    /// Asynchronously converts a WAV (or any PCM) stream into an Opus file.
    /// </summary>
    /// <param name="inputStream">
    ///   Any Stream (e.g. FileStream, MemoryStream, NetworkStream) containing raw audio 
    ///   FFmpeg can decode (here assumed WAV/PCM).
    /// </param>
    /// <param name="outputFileName">
    ///   The name of the output file (without path).
    /// </param>
    /// <param name="cancellationToken">
    ///   CancellationToken to abort the operation mid‐stream.
    /// </param>
    /// <returns>
    ///   The absolute path to the generated Opus file.
    /// </returns>
    public static async Task<string> ConvertWavToOpusFileAsync(
        Stream inputStream,
        string outputFileName,
        CancellationToken cancellationToken = default)
    {
        if (inputStream == null) throw new ArgumentNullException(nameof(inputStream));
        if (string.IsNullOrWhiteSpace(outputFileName))
            throw new ArgumentException("Output file name cannot be empty", nameof(outputFileName));

        // Ensure the output directory exists
        string outputDir = "/tmp/vcecht";
        Directory.CreateDirectory(outputDir);
        string outputPath = Path.Combine(outputDir, outputFileName);

        // Build ffmpeg arguments (quiet, read stdin, write to file in Opus)
        string ffmpegArgs = string.Join(" ", new[]
        {
            "-hide_banner",
            "-loglevel", "error",
            "-nostdin",
            "-i", "pipe:0",
            "-c:a", "libopus",
            "-b:a", "16k",
            "-compression_level", "10",
            "-vbr", "on",
            "-application", "voip",
            "-frame_duration", "20",
            "-movflags", "+faststart",
            "-ar", "24000",
            "-f", "Opus",
            $"\"{outputPath}\"" // Output to file instead of stdout
        });

        var psi = new ProcessStartInfo
        {
            FileName            = "ffmpeg",
            Arguments           = ffmpegArgs,
            UseShellExecute     = false,
            RedirectStandardInput  = true,
            RedirectStandardError  = true,
            CreateNoWindow      = true
        };

        var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };

        try
        {
            if (!ffmpeg.Start())
                throw new InvalidOperationException("Failed to start FFmpeg process.");

            // Write inputStream → ffmpeg.StandardInput.BaseStream
            await Task.Run(async () =>
            {
                try
                {
                    Stream stdinPipe = ffmpeg.StandardInput.BaseStream;
                    await inputStream.CopyToAsync(stdinPipe, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
                    // Signal EOF to ffmpeg
                    ffmpeg.StandardInput.Close();
                }
                catch (Exception ex) when (!(ex is OperationCanceledException))
                {
                    // If writing fails, kill the process so it doesn't hang
                    try { ffmpeg.Kill(); } catch { /* ignore */ }
                    throw;
                }
            }, cancellationToken);

            // Read stderr to capture any error text
            string stderrText = await Task.Run(async () =>
            {
                using (var sr = ffmpeg.StandardError)
                {
                    return await sr.ReadToEndAsync().ConfigureAwait(false);
                }
            }, cancellationToken);

            // Wait for ffmpeg to finish
            await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);

            if (ffmpeg.ExitCode != 0)
            {
                throw new InvalidOperationException(
                    $"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
            }

            // Verify the file was created
            if (!File.Exists(outputPath))
            {
                throw new InvalidOperationException("FFmpeg did not create the expected output file.");
            }

            return outputPath;
        }
        finally
        {
            ffmpeg.Dispose();
        }
    }

    /// <summary>
    /// Reads a file from the specified path and returns it as a memory stream.
    /// </summary>
    /// <param name="filePath">The path to the file to read.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A MemoryStream containing the file contents.</returns>
    /// <exception cref="ArgumentNullException">Thrown when filePath is null or empty.</exception>
    /// <exception cref="FileNotFoundException">Thrown when the specified file does not exist.</exception>
    public static async Task<MemoryStream> ReadFileToStreamAsync(string filePath, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(filePath))
            throw new ArgumentNullException(nameof(filePath));

        if (!File.Exists(filePath))
            throw new FileNotFoundException($"The file '{filePath}' was not found.", filePath);

        var memoryStream = new MemoryStream();
        try
        {
            using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, FileOptions.Asynchronous | FileOptions.SequentialScan))
            {
                await fileStream.CopyToAsync(memoryStream, 81920, cancellationToken).ConfigureAwait(false);
            }
            memoryStream.Position = 0; // Reset position to the beginning of the stream
            return memoryStream;
        }
        catch
        {
            await memoryStream.DisposeAsync();
            throw;
        }
    }

    /// <summary>
    /// Writes the contents of a stream to the specified file path.
    /// </summary>
    /// <param name="stream">The stream to write to the file.</param>
    /// <param name="filePath">The path where the file will be written.</param>
    /// <param name="overwrite">Whether to overwrite the file if it already exists. Default is true.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A task that represents the asynchronous write operation.</returns>
    /// <exception cref="ArgumentNullException">Thrown when stream or filePath is null or empty.</exception>
    /// <exception cref="IOException">Thrown when the file already exists and overwrite is false.</exception>
    public static async Task WriteStreamToFileAsync(Stream stream, string filePath, bool overwrite = true, CancellationToken cancellationToken = default)
    {
        if (stream == null)
            throw new ArgumentNullException(nameof(stream));
        if (string.IsNullOrWhiteSpace(filePath))
            throw new ArgumentNullException(nameof(filePath));

        // Ensure the directory exists
        var directory = Path.GetDirectoryName(filePath);
        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        if (File.Exists(filePath))
        {
            if (!overwrite)
                throw new IOException($"The file '{filePath}' already exists and overwrite is set to false.");
            
            File.Delete(filePath);
        }

        using (var fileStream = new FileStream(
            filePath, 
            FileMode.CreateNew, 
            FileAccess.Write, 
            FileShare.None, 
            81920, 
            FileOptions.Asynchronous | FileOptions.SequentialScan))
        {
            await stream.CopyToAsync(fileStream, 81920, cancellationToken).ConfigureAwait(false);
            await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
        }
    }

    /// <summary>
    /// Converts an audio file to Opus format by reading and writing directly from/to files.
    /// </summary>
    /// <param name="inputFilePath">Path to the input audio file.</param>
    /// <param name="outputFilePath">Path where the output Opus file will be saved.</param>
    /// <param name="cancellationToken">CancellationToken to abort the operation.</param>
    /// <returns>The path to the converted file (same as outputFilePath).</returns>
    public static async Task<string> ConvertFileToOpusFileAsync(
        string inputFilePath,
        string outputFilePath,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(inputFilePath))
            throw new ArgumentNullException(nameof(inputFilePath));
        if (string.IsNullOrWhiteSpace(outputFilePath))
            throw new ArgumentNullException(nameof(outputFilePath));
        if (!File.Exists(inputFilePath))
            throw new FileNotFoundException($"Input file not found: {inputFilePath}", inputFilePath);

        // Ensure the output directory exists
        string outputDir = Path.GetDirectoryName(outputFilePath);
        if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
        {
            Directory.CreateDirectory(outputDir);
        }

        // Build ffmpeg arguments
        string ffmpegArgs = string.Join(" ", new[]
        {
            "-hide_banner",
            "-loglevel", "error",
            "-i", $"\"{inputFilePath}\"",
            "-c:a", "libopus",
            "-b:a", "16k",
            "-compression_level", "10",
            "-vbr", "on",
            "-application", "voip",
            "-frame_size", "20",
            "-deadline", "realtime",
            "-ar", "24000",
            "-f", "opus",
            "-fflags"," +genpts",
            $"\"{outputFilePath}\""
        });

        var psi = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = ffmpegArgs,
            UseShellExecute = false,
            RedirectStandardError = true,
            CreateNoWindow = true
        };

        using var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };

        if (!ffmpeg.Start())
            throw new InvalidOperationException("Failed to start FFmpeg process.");

        // Read stderr to capture any error text
        string stderrText = await Task.Run(async () =>
        {
            using var sr = ffmpeg.StandardError;
            return await sr.ReadToEndAsync().ConfigureAwait(false);
        }, cancellationToken);

        // Wait for ffmpeg to finish
        await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);

        if (ffmpeg.ExitCode != 0)
        {
            throw new InvalidOperationException(
                $"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
        }

        if (!File.Exists(outputFilePath))
        {
            throw new InvalidOperationException("FFmpeg did not create the expected output file.");
        }

        return outputFilePath;
    }

    /// <summary>
    /// Converts an audio file to Opus format and returns it as a stream.
    /// </summary>
    /// <param name="inputFilePath">Path to the input audio file.</param>
    /// <param name="cancellationToken">CancellationToken to abort the operation.</param>
    /// <returns>A MemoryStream containing the converted audio in Opus format.</returns>
    public static async Task<Stream> ConvertFileToOpusStreamAsync(
        string inputFilePath,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(inputFilePath))
            throw new ArgumentNullException(nameof(inputFilePath));
        if (!File.Exists(inputFilePath))
            throw new FileNotFoundException($"Input file not found: {inputFilePath}", inputFilePath);

        // Build ffmpeg arguments
        string ffmpegArgs = string.Join(" ", new[]
        {
            "-hide_banner",
            "-loglevel", "error",
            "-i", $"\"{inputFilePath}\"",
            "-c:a", "libopus",
            "-b:a", "16k",
            "-compression_level", "10",
            "-vbr", "on",
            "-application", "voip",
            "-frame_size", "20",
            "-deadline", "realtime",
            "-ar", "24000",
            "-f", "opus",
            "-fflags"," +genpts",
            "pipe:1"
        });

        var psi = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = ffmpegArgs,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true
        };

        var outputMem = new MemoryStream();
        var ffmpeg = new Process { StartInfo = psi, EnableRaisingEvents = true };

        try
        {
            if (!ffmpeg.Start())
                throw new InvalidOperationException("Failed to start FFmpeg process.");

            // Read stdout into memory stream
            await Task.Run(async () =>
            {
                try
                {
                    await ffmpeg.StandardOutput.BaseStream.CopyToAsync(outputMem, 81920, cancellationToken).ConfigureAwait(false);
                }
                catch (Exception ex) when (!(ex is OperationCanceledException))
                {
                    try { ffmpeg.Kill(); } catch { /* ignore */ }
                    throw;
                }
            }, cancellationToken);

            // Read stderr to capture any error text
            string stderrText = await Task.Run(async () =>
            {
                using var sr = ffmpeg.StandardError;
                return await sr.ReadToEndAsync().ConfigureAwait(false);
            }, cancellationToken);

            // Wait for ffmpeg to finish
            await Task.Run(() => ffmpeg.WaitForExit(), cancellationToken).ConfigureAwait(false);

            if (ffmpeg.ExitCode != 0)
            {
                throw new InvalidOperationException(
                    $"FFmpeg exited with code {ffmpeg.ExitCode}. Details:\n{stderrText}");
            }

            outputMem.Position = 0;
            return outputMem;
        }
        catch
        {
            await outputMem.DisposeAsync();
            throw;
        }
        finally
        {
            ffmpeg.Dispose();
        }
    }
}