using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Media.Capture;
using Windows.Media.Capture.Frames;
using Windows.Media.Devices;
using Windows.Media.MediaProperties;
using ytLive.Helpers;

namespace ytLive.Services;

/// <summary>
/// CPU-first MediaCapture source. Requests BGRA8 frames from the camera's video
/// preview source; the capture pipeline does any format conversion, so every
/// frame arrives as a normalized <see cref="VideoFrame"/>. Frames arrive on a
/// worker thread — marshal before touching WPF.
///
/// Allocation is validated, never taken on faith: after <c>InitializeAsync</c>
/// the bound device, its frame sources, and its stream properties are checked,
/// the frame reader's start status is read (not swallowed), and the live
/// state signals (<c>Failed</c>, <c>CameraStreamStateChanged</c>) are subscribed
/// so an async death surfaces as <see cref="SourceFailed"/> instead of a silent
/// empty preview.
/// </summary>
public sealed class MediaCaptureFrameSource : ICameraFrameSource
{
    private readonly string _deviceId;
    private MediaCapture? _capture;
    private MediaFrameReader? _frameReader;
    private string? _lastError;
    private bool _isFailed;

    public string DeviceId => _deviceId;

    public event Action<VideoFrame>? FrameAvailable;

    public event Action<string>? SourceFailed;

    public MediaCaptureFrameSource(string deviceId)
    {
        _deviceId = deviceId;
    }

    public async Task StartAsync()
    {
        // Fallback ladder: prefer the camera's VideoPreview stream, but if the
        // reader refuses to start there (NoVideoFrameAvailable), retry against
        // its VideoRecord stream — some devices only deliver through it.
        // (MediaCaptureSharingMode.Exclusive is not projected by the 19041 SDK.)
        if (!await TryStartAsync(preferVideoRecord: false))
        {
            if (!await TryStartAsync(preferVideoRecord: true))
                throw new InvalidOperationException(
                    _lastError ?? $"Camera '{_deviceId}' could not be started.");
        }
    }

    public async Task StopAsync()
    {
        var reader = _frameReader;
        _frameReader = null;
        if (reader != null)
        {
            reader.FrameArrived -= OnFrameArrived;
            try
            {
                await reader.StopAsync();
            }
            catch (Exception ex)
            {
                AppLog.Write($"MediaCaptureFrameSource: stop frame reader failed: {ex.Message}");
            }
            reader.Dispose();
        }

        var capture = _capture;
        _capture = null;
        if (capture != null)
        {
            UnsubscribeCaptureEvents(capture);
            capture.Dispose();
        }
    }

    private async Task<bool> TryStartAsync(bool preferVideoRecord)
    {
        MediaCapture? capture = null;
        MediaFrameReader? reader = null;
        try
        {
            capture = new MediaCapture();
            var settings = new MediaCaptureInitializationSettings
            {
                VideoDeviceId = _deviceId,
                StreamingCaptureMode = StreamingCaptureMode.Video,
                MemoryPreference = MediaCaptureMemoryPreference.Cpu,
                SharingMode = MediaCaptureSharingMode.SharedReadOnly,
            };
            await capture.InitializeAsync(settings);

            // Post-init resource validation: the capture must actually be bound to
            // the device we asked for and must expose a live video controller.
            if (!string.Equals(capture.MediaCaptureSettings?.VideoDeviceId, _deviceId,
                    StringComparison.OrdinalIgnoreCase))
                throw new InvalidOperationException(
                    $"Camera '{_deviceId}' initialized but bound a different device.");

            var colorSource = capture.FrameSources.Values
                .OrderBy(s => preferVideoRecord
                    ? (s.Info.MediaStreamType == MediaStreamType.VideoRecord ? 0 : 1)
                    : (s.Info.MediaStreamType == MediaStreamType.VideoPreview ? 0 : 1))
                .FirstOrDefault(s => s.Info.MediaStreamType is MediaStreamType.VideoPreview or MediaStreamType.VideoRecord);
            if (colorSource == null)
            {
                var kinds = string.Join(", ", capture.FrameSources.Values
                    .Select(s => s.Info.MediaStreamType).Distinct());
                throw new InvalidOperationException(
                    $"Camera '{_deviceId}' exposes no video source (found: {kinds}). " +
                    "It may be locked by another app (e.g. NVIDIA Broadcast).");
            }

            // The device must answer for its preview stream — a dead, suspended, or
            // locked device returns no stream properties even after "successful" init.
            IReadOnlyList<IMediaEncodingProperties>? previewProps;
            try
            {
                previewProps = capture.VideoDeviceController?
                    .GetAvailableMediaStreamProperties(MediaStreamType.VideoPreview);
            }
            catch
            {
                previewProps = null;
            }
            if (previewProps == null || previewProps.Count == 0)
                throw new InvalidOperationException(
                    $"Camera '{_deviceId}' answered no video stream properties (offline, suspended, or locked).");

            // The OS's live state signals: async failures must surface, not vanish.
            capture.Failed += OnCaptureFailed;
            capture.CameraStreamStateChanged += OnCameraStreamStateChanged;

            reader = await capture.CreateFrameReaderAsync(colorSource, MediaEncodingSubtypes.Bgra8);
            if (reader == null)
                throw new InvalidOperationException($"Camera '{_deviceId}' created no frame reader.");

            reader.FrameArrived += OnFrameArrived;
            var status = await reader.StartAsync();
            if (status != MediaFrameReaderStartStatus.Success)
                throw new InvalidOperationException(
                    $"Camera '{_deviceId}' frame reader refused to start: {status}.");

            _capture = capture;
            _frameReader = reader;
            _lastError = null;
            _isFailed = false;
            return true;
        }
        catch (Exception ex)
        {
            _lastError = ex.Message;
            if (reader != null)
            {
                reader.FrameArrived -= OnFrameArrived;
                reader.Dispose();
            }
            if (capture != null)
            {
                UnsubscribeCaptureEvents(capture);
                capture.Dispose();
            }
            return false;
        }
    }

    private void UnsubscribeCaptureEvents(MediaCapture capture)
    {
        capture.Failed -= OnCaptureFailed;
        capture.CameraStreamStateChanged -= OnCameraStreamStateChanged;
    }

    private void OnCaptureFailed(MediaCapture sender, MediaCaptureFailedEventArgs args)
        => RaiseFailure($"capture failed ({args.Code}): {args.Message}");

    private void OnCameraStreamStateChanged(MediaCapture sender, object args)
    {
        // CameraStreamState enum (Windows.Media.Devices): 0=NotStreaming, 1=Streaming,
        // 2=Failed, 3=Shutdown. The 19041 SDK projection omits member names, so
        // compare by value — the enum type itself resolves via the property return.
        if ((int)sender.CameraStreamState == 2) // Failed
            RaiseFailure("camera stream state is Failed");
    }

    private void RaiseFailure(string message)
    {
        if (_isFailed) return;
        _isFailed = true;
        AppLog.Write($"MediaCaptureFrameSource: {message}");
        try
        {
            SourceFailed?.Invoke(message);
        }
        catch (Exception ex)
        {
            AppLog.Write($"MediaCaptureFrameSource: SourceFailed handler threw: {ex.Message}");
        }
    }

    private void OnFrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
    {
        using var frame = sender.TryAcquireLatestFrame();
        var videoFrame = frame?.VideoMediaFrame?.SoftwareBitmap;
        if (videoFrame == null) return;

        var bitmap = videoFrame.BitmapPixelFormat == BitmapPixelFormat.Bgra8
            ? videoFrame
            : SoftwareBitmap.Convert(videoFrame, BitmapPixelFormat.Bgra8);

        try
        {
            using var buffer = bitmap.LockBuffer(BitmapBufferAccessMode.Read);
            using var reference = buffer.CreateReference();
            if (WindowsRuntimeMarshal.TryGetDataUnsafe(reference, out var pixelsPtr, out var capacity))
            {
                var pixels = new byte[capacity];
                Marshal.Copy(pixelsPtr, pixels, 0, (int)capacity);
                FrameAvailable?.Invoke(new VideoFrame(bitmap.PixelWidth, bitmap.PixelHeight, pixels));
            }
        }
        catch (Exception ex)
        {
            AppLog.Write($"MediaCaptureFrameSource: frame copy failed: {ex.Message}");
        }
        finally
        {
            if (!ReferenceEquals(bitmap, videoFrame))
                bitmap.Dispose();
        }
    }
}
