using System.IO.Pipelines;
using System.Text;
using CliWrap;
using ErsatzTV.Application.Streaming;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Streaming;
using Microsoft.AspNetCore.Mvc;

namespace ErsatzTV.Controllers;

public abstract class StreamingControllerBase(IGraphicsEngine graphicsEngine, ILogger logger)
    : ControllerBase
{
    protected IActionResult GetProcessResponse(
        Either<BaseError, PlayoutItemProcessModel> result,
        string channelNumber,
        StreamingMode mode,
        IDirectStreamSessionTracker directStreamSessionTracker = null)
    {
        foreach (BaseError error in result.LeftToSeq())
        {
            logger.LogError(
                "Failed to create stream for channel {ChannelNumber}: {Error}",
                channelNumber,
                error.Value);

            return BadRequest(error.Value);
        }

        foreach (PlayoutItemProcessModel processModel in result.RightToSeq())
        {
            return StartPlayout(processModel, channelNumber, mode, directStreamSessionTracker);
        }

        // this will never happen
        return new NotFoundResult();
    }

    private FileStreamResult StartPlayout(
        PlayoutItemProcessModel processModel,
        string channelNumber,
        StreamingMode mode,
        IDirectStreamSessionTracker directStreamSessionTracker)
    {
        // for process counter
        var ffmpegProcess = new FFmpegProcess();
        Command process = processModel.Process;

        logger.LogDebug("ffmpeg arguments {FFmpegArguments}", process.Arguments);

        var cts = new CancellationTokenSource();

        // do not use 'using' here; the token needs to live longer than this method scope
        var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            cts.Token,
            HttpContext.RequestAborted);

        var pipe = new Pipe();
        var stdErrBuffer = new StringBuilder();

        Command processWithPipe = process;
        foreach (GraphicsEngineContext graphicsEngineContext in processModel.GraphicsEngineContext)
        {
            var gePipe = new Pipe();
            processWithPipe = process.WithStandardInputPipe(PipeSource.FromStream(gePipe.Reader.AsStream()));

            // fire and forget graphics engine task
            _ = graphicsEngine.Run(
                graphicsEngineContext,
                gePipe.Writer,
                linkedCts.Token);
        }

        CommandTask<CommandResult> task = processWithPipe
            .WithStandardOutputPipe(PipeTarget.ToStream(pipe.Writer.AsStream()))
            .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
            .WithValidation(CommandResultValidation.None)
            .ExecuteAsync(linkedCts.Token);

        // ensure cleanup happens when ffmpeg exits (either naturally or via cancellation)
        _ = task.Task.ContinueWith(
            (t, _) =>
            {
                pipe.Writer.Complete(t.Exception);
                ffmpegProcess.Dispose();
                linkedCts.Dispose();
                cts.Dispose();
            },
            null,
            TaskScheduler.Default);

        Stream stream = pipe.Reader.AsStream();

        return directStreamSessionTracker is null
            ? new FileStreamResult(stream, "video/mp2t")
            : new TrackedFileStreamResult(stream, "video/mp2t", directStreamSessionTracker, channelNumber, mode);
    }
}
