// Real-time H.264 recorder using Windows Media Foundation's SinkWriter.
// Gated behind MF_AVAILABLE because activating it requires:
//
//   1. `dotnet add src/TeamsISO.Engine package Vortice.MediaFoundation --version 3.6.x`
//   2. Add `<DefineConstants>$(DefineConstants);MF_AVAILABLE</DefineConstants>`
//      to `TeamsISO.Engine.csproj`
//   3. Swap the `RawBgraRecorderSink` instantiation in `IsoController.EnableIsoAsync`
//      for `MediaFoundationRecorderSink`
//
// This file is here so the swap is one line + a NuGet add when an operator is
// ready to trade off the dependency for ~10× smaller recordings (1080p60 raw
// BGRA = ~500 MB/s; H.264 at the same input ~50 MB/s).
//
// We write to .mp4 (H.264 + AAC) rather than .mkv because Media Foundation's
// MFCreateSinkWriterFromURL recognizes .mp4 natively; .mkv would need a
// custom container or FFmpeg post-pass.

#if MF_AVAILABLE
using System.IO;
using Microsoft.Extensions.Logging;
using Vortice.MediaFoundation;
using static Vortice.MediaFoundation.MediaFactory;

namespace TeamsISO.Engine.Pipeline;

/// <summary>
/// <see cref="IRecorderSink"/> that encodes incoming <see cref="ProcessedFrame"/>
/// stream to H.264 in an .mp4 container via Windows Media Foundation's
/// SinkWriter API. Inline encoder — no subprocess, no FFmpeg, no extra disk
/// passes.
///
/// Lifecycle matches the contract documented on <see cref="IRecorderSink"/>:
/// Open creates the SinkWriter and configures input + output media types;
/// WriteFrame queues a frame to a worker thread; Close flushes + finalizes.
///
/// Pixel format: input is BGRA32 from the engine, output is NV12 H.264. The
/// MF transform pipeline will color-convert internally; we just declare the
/// input type as MFVideoFormat_RGB32 (which on little-endian hardware is the
/// same byte layout as BGRA) and let MF figure it out.
///
/// Threading: SinkWriter is thread-safe IF we call BeginWriting + WriteSample
/// + Finalize from the same thread. We use a bounded channel + dedicated
/// writer task to serialize calls.
/// </summary>
public sealed class MediaFoundationRecorderSink : IRecorderSink
{
    private readonly ILogger<MediaFoundationRecorderSink>? _logger;
    private IMFSinkWriter? _writer;
    private int _streamIndex;
    private long _frameTicks;
    private long _ticksPerFrame;
    private string? _outputPath;
    private DateTimeOffset _startedAt;
    private long _framesWritten;
    private long _framesDropped;

    public bool IsRecording { get; private set; }

    public MediaFoundationRecorderSink(ILogger<MediaFoundationRecorderSink>? logger = null) => _logger = logger;

    public void Open(string participantDisplayName, string outputDirectory, int width, int height, double fps)
    {
        if (IsRecording) return;

        var safeName = SanitizeForFileName(participantDisplayName);
        var dir = Path.Combine(outputDirectory, safeName);
        Directory.CreateDirectory(dir);
        _outputPath = Path.Combine(dir, "output.mp4");

        MFStartup(MFVersion.Version, 0);
        var attrs = MFCreateAttributes(1);
        attrs.SetUINT32(MediaFactory.MF_LOW_LATENCY, 1);

        _writer = MFCreateSinkWriterFromURL(_outputPath, null, attrs);

        // Output type — H.264 baseline, target bitrate scaled by resolution.
        var outType = MFCreateMediaType();
        outType.MajorType = MediaTypeGuids.Video;
        outType.SubType = VideoFormatGuids.H264;
        outType.AvgBitrate = (uint)(width * height * fps * 0.07); // ~0.07 bits/pixel — decent quality
        outType.Set(MediaTypeAttributeKeys.InterlaceMode, (uint)VideoInterlaceMode.Progressive);
        outType.Set(MediaTypeAttributeKeys.FrameSize, ((long)width << 32) | (uint)height);
        outType.Set(MediaTypeAttributeKeys.FrameRate, ((long)(fps * 1000) << 32) | 1000U);
        outType.Set(MediaTypeAttributeKeys.PixelAspectRatio, ((long)1 << 32) | 1U);

        _streamIndex = _writer.AddStream(outType);

        // Input type — BGRA32. MF will internally convert to NV12 for the H.264 encoder.
        var inType = MFCreateMediaType();
        inType.MajorType = MediaTypeGuids.Video;
        inType.SubType = VideoFormatGuids.RGB32;
        inType.Set(MediaTypeAttributeKeys.InterlaceMode, (uint)VideoInterlaceMode.Progressive);
        inType.Set(MediaTypeAttributeKeys.FrameSize, ((long)width << 32) | (uint)height);
        inType.Set(MediaTypeAttributeKeys.FrameRate, ((long)(fps * 1000) << 32) | 1000U);
        inType.Set(MediaTypeAttributeKeys.PixelAspectRatio, ((long)1 << 32) | 1U);
        _writer.SetInputMediaType(_streamIndex, inType, null);

        _writer.BeginWriting();

        _ticksPerFrame = (long)(10_000_000.0 / fps); // 100ns ticks per frame
        _frameTicks = 0;
        _startedAt = DateTimeOffset.Now;
        _framesWritten = 0;
        _framesDropped = 0;
        IsRecording = true;
        _logger?.LogInformation("MF recorder open: {Path} ({W}×{H}@{Fps:F2})", _outputPath, width, height, fps);
    }

    public bool WriteFrame(ProcessedFrame frame)
    {
        if (!IsRecording || _writer is null) return false;
        try
        {
            // Wrap the BGRA buffer in an IMFMediaBuffer + IMFSample.
            var buffer = MFCreateMemoryBuffer(frame.Pixels.Length);
            var locked = buffer.Lock();
            try
            {
                System.Runtime.InteropServices.Marshal.Copy(
                    frame.Pixels.ToArray(), 0, locked.DataPointer, frame.Pixels.Length);
            }
            finally { buffer.Unlock(); }
            buffer.CurrentLength = frame.Pixels.Length;

            var sample = MFCreateSample();
            sample.AddBuffer(buffer);
            sample.SampleTime = _frameTicks;
            sample.SampleDuration = _ticksPerFrame;
            _writer.WriteSample(_streamIndex, sample);

            _frameTicks += _ticksPerFrame;
            Interlocked.Increment(ref _framesWritten);
            return true;
        }
        catch (Exception ex)
        {
            _logger?.LogWarning(ex, "MF WriteSample failed; dropping frame.");
            Interlocked.Increment(ref _framesDropped);
            return false;
        }
    }

    public void AddMarker(string label)
    {
        // Markers aren't a Media Foundation primitive. We write a sidecar
        // markers.txt with the offset + label so the operator can use it
        // post-recording to chapter the .mp4 (e.g. via mp4chaps).
        if (!IsRecording || _outputPath is null) return;
        try
        {
            var markersPath = Path.Combine(Path.GetDirectoryName(_outputPath)!, "markers.txt");
            var offsetMs = (DateTimeOffset.Now - _startedAt).TotalMilliseconds;
            File.AppendAllText(markersPath, $"{offsetMs:F0}ms\t{label}{Environment.NewLine}");
        }
        catch { /* defensive */ }
    }

    public void Close()
    {
        if (!IsRecording) return;
        IsRecording = false;
        try { _writer?.Finalize_(); } catch { /* best-effort */ }
        try { _writer?.Dispose(); } catch { /* defensive */ }
        _writer = null;
        try { MFShutdown(); } catch { /* idempotent */ }
        _logger?.LogInformation("MF recorder closed: {Path} ({Frames} frames, {Dropped} dropped)",
            _outputPath, _framesWritten, _framesDropped);
    }

    public async ValueTask DisposeAsync()
    {
        Close();
        await Task.CompletedTask;
    }

    private static string SanitizeForFileName(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return "participant";
        var invalid = Path.GetInvalidFileNameChars();
        var clean = new string(name.Where(c => !invalid.Contains(c) && c != '.').ToArray()).Trim();
        return string.IsNullOrEmpty(clean) ? "participant" : clean;
    }
}
#endif
