using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using ytLive.Helpers;

namespace ytLive.Services;

/// <summary>
/// Owns webcam capture sessions app-wide, refcounted by DeviceId. One device =
/// at most one capture, one shared WriteableBitmap; the last release stops and
/// disposes the source. Frames arrive on a worker thread and are coalesced onto
/// the UI dispatcher (at most one pending copy per session, using the latest
/// frame) so a 60fps device doesn't drown the render thread.
///
/// Allocation is proven, never assumed: <see cref="AcquireAsync"/> treats a
/// started reader as success only once the first frame actually arrives (within
/// <paramref name="firstFrameTimeout"/>), and an async <see cref="SourceFailed"/>
/// tears the session down and surfaces <see cref="CameraFailed"/> — no silent
/// empty box.
/// </summary>
public sealed class CameraManager : IDisposable
{
    private sealed class CameraSession
    {
        public string DeviceId { get; }
        public ICameraFrameSource Source { get; }
        public Action<VideoFrame>? FrameHandler;
        public Action<string>? FailureHandler;
        public int RefCount;
        public bool Started;
        public WriteableBitmap? PreviewBitmap;
        public VideoFrame? LatestFrame;
        public bool FramePending;
        public TaskCompletionSource<bool>? FirstFrame;

        public CameraSession(string deviceId, ICameraFrameSource source)
        {
            DeviceId = deviceId;
            Source = source;
            RefCount = 1;
        }
    }

    private readonly ICameraEnumerator _enumerator;
    private readonly Func<string, ICameraFrameSource> _frameSourceFactory;
    private readonly Dispatcher? _uiDispatcher;
    private readonly TimeSpan _firstFrameTimeout;
    private readonly Dictionary<string, CameraSession> _sessions = new();
    private readonly object _gate = new();

    /// <summary>Raised on the UI thread when a camera's shared preview bitmap is first created.</summary>
    public event Action<string, WriteableBitmap>? PreviewBitmapChanged;

    /// <summary>
    /// Raised when a capture fails: device in use, access denied, no preview source,
    /// or a started reader that never delivers a first frame within the timeout.
    /// </summary>
    public event Action<string, string>? CameraFailed;

    public CameraManager(ICameraEnumerator enumerator, Func<string, ICameraFrameSource> frameSourceFactory,
        Dispatcher? uiDispatcher = null, TimeSpan? firstFrameTimeout = null)
    {
        _enumerator = enumerator;
        _frameSourceFactory = frameSourceFactory;
        _uiDispatcher = uiDispatcher;
        _firstFrameTimeout = firstFrameTimeout ?? TimeSpan.FromSeconds(4);
    }

    public ICameraEnumerator Enumerator => _enumerator;

    /// <summary>Increments the refcount for a device, starting capture the first time.</summary>
    public async Task<bool> AcquireAsync(string deviceId)
    {
        if (string.IsNullOrWhiteSpace(deviceId)) return false;

        CameraSession session;
        bool shouldStart;
        lock (_gate)
        {
            if (_sessions.TryGetValue(deviceId, out var existing))
            {
                existing.RefCount++;
                shouldStart = false;
                session = existing;
            }
            else
            {
                session = new CameraSession(deviceId, _frameSourceFactory(deviceId));
                session.FrameHandler = frame => OnFrameAvailable(session, frame);
                session.Source.FrameAvailable += session.FrameHandler;
                session.FailureHandler = message => OnSourceFailed(session, message);
                session.Source.SourceFailed += session.FailureHandler;
                _sessions[deviceId] = session;
                shouldStart = true;
            }
        }

        if (!shouldStart) return session.Started;

        session.FirstFrame = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        try
        {
            await session.Source.StartAsync();

            // First-frame proof: "the reader started" is not "the stream is live".
            // Success requires an actual frame within the timeout, else the session
            // is rolled back and reported — never left as a silent empty preview.
            var proven = _firstFrameTimeout <= TimeSpan.Zero
                || await WaitForFirstFrameAsync(session, _firstFrameTimeout);
            if (!proven)
            {
                var reason = $"Camera '{deviceId}' started but produced no frames within {_firstFrameTimeout.TotalSeconds:0.#}s.";
                RollbackSession(session, reason);
                return false;
            }

            // Keep the SourceFailed handler subscribed: an async failure after a
            // successful start (device lost, stream state Failed) must still surface.
            session.FirstFrame = null;
            session.Started = true;
            return true;
        }
        catch (Exception ex)
        {
            RollbackSession(session, ex.Message);
            return false;
        }
    }

    /// <summary>Decrements the refcount; stops and disposes the source at zero.</summary>
    public async Task ReleaseAsync(string deviceId)
    {
        CameraSession? toStop = null;
        lock (_gate)
        {
            if (!_sessions.TryGetValue(deviceId, out var session)) return;
            if (--session.RefCount > 0) return;
            _sessions.Remove(deviceId);
            toStop = session;
        }

        if (toStop == null) return;
        toStop.Source.FrameAvailable -= toStop.FrameHandler;
        await SafeStopAsync(toStop.Source);
        toStop.PreviewBitmap = null;
    }

    /// <summary>
    /// Releases a device unconditionally (every ref), regardless of how many scenes
    /// hold it. Used on identity changes (webcam swap, layout reload) where the old
    /// device's refcount isn't known after the scenes are replaced.
    /// </summary>
    public async Task ReleaseAllAsync(string deviceId)
    {
        CameraSession? toStop = null;
        lock (_gate)
        {
            if (!_sessions.TryGetValue(deviceId, out var session)) return;
            session.RefCount = 0;
            _sessions.Remove(deviceId);
            toStop = session;
        }

        if (toStop == null) return;
        toStop.Source.FrameAvailable -= toStop.FrameHandler;
        await SafeStopAsync(toStop.Source);
        toStop.PreviewBitmap = null;
    }

    public VideoFrame? GetLatestFrame(string deviceId)
    {
        lock (_gate)
            return _sessions.TryGetValue(deviceId, out var session) ? session.LatestFrame : null;
    }

    /// <summary>
    /// The shared WriteableBitmap for a running session, or null when the camera
    /// hasn't produced its first frame yet. <see cref="PreviewBitmapChanged"/> only
    /// fires once (first frame), so configs created after the session started must
    /// pick the bitmap up from here.
    /// </summary>
    public WriteableBitmap? GetPreviewBitmap(string deviceId)
    {
        lock (_gate)
            return _sessions.TryGetValue(deviceId, out var session) ? session.PreviewBitmap : null;
    }

    public void Dispose()
    {
        List<CameraSession> sessions;
        lock (_gate)
        {
            sessions = new List<CameraSession>(_sessions.Values);
            _sessions.Clear();
        }

        foreach (var session in sessions)
        {
            session.Source.FrameAvailable -= session.FrameHandler;
            if (session.FailureHandler != null)
                session.Source.SourceFailed -= session.FailureHandler;
            _ = SafeStopAsync(session.Source);
        }
    }

    private static async Task<bool> WaitForFirstFrameAsync(CameraSession session, TimeSpan timeout)
    {
        var first = session.FirstFrame;
        if (first == null) return true;
        var completed = await Task.WhenAny(first.Task, Task.Delay(timeout));
        return ReferenceEquals(completed, first.Task) && first.Task.Result;
    }

    private void RollbackSession(CameraSession session, string message)
    {
        lock (_gate)
        {
            if (TryGetActiveSession(session))
                _sessions.Remove(session.DeviceId);
        }
        session.Source.FrameAvailable -= session.FrameHandler;
        if (session.FailureHandler != null)
            session.Source.SourceFailed -= session.FailureHandler;
        session.FirstFrame?.TrySetResult(false);

        var enriched = message;
        var suspects = CameraConflictProbe.GetRunningCameraApps();
        if (suspects.Count > 0)
            enriched += $" Other camera apps running: {string.Join(", ", suspects)}.";

        AppLog.Write($"CameraManager: camera '{session.DeviceId}' failed: {enriched}");
        CameraFailed?.Invoke(session.DeviceId, enriched);
        _ = SafeStopAsync(session.Source);
    }

    private void OnSourceFailed(CameraSession session, string message)
    {
        RollbackSession(session, message);
    }

    private static async Task SafeStopAsync(ICameraFrameSource source)
    {
        try
        {
            await source.StopAsync();
        }
        catch (Exception ex)
        {
            AppLog.Write($"CameraManager: stopping camera failed: {ex.Message}");
        }
    }

    private bool TryGetActiveSession(CameraSession session)
    {
        lock (_gate)
            return _sessions.TryGetValue(session.DeviceId, out var current) && ReferenceEquals(current, session);
    }

    private void OnFrameAvailable(CameraSession session, VideoFrame frame)
    {
        if (!TryGetActiveSession(session)) return;
        session.LatestFrame = frame;
        session.FirstFrame?.TrySetResult(true);

        if (session.PreviewBitmap == null)
        {
            if (_uiDispatcher == null) return;
            if (_uiDispatcher.CheckAccess())
                EnsurePreviewBitmap(session);
            else
                _uiDispatcher.BeginInvoke(() =>
                {
                    if (TryGetActiveSession(session))
                        EnsurePreviewBitmap(session);
                }, DispatcherPriority.Render);
            return;
        }

        if (_uiDispatcher == null || _uiDispatcher.CheckAccess())
        {
            CopyFrame(session);
            return;
        }

        if (session.FramePending) return;
        session.FramePending = true;
        _uiDispatcher.BeginInvoke(() =>
        {
            session.FramePending = false;
            if (TryGetActiveSession(session) && session.PreviewBitmap != null)
                CopyFrame(session);
        }, DispatcherPriority.Render);
    }

    private void EnsurePreviewBitmap(CameraSession session)
    {
        if (session.PreviewBitmap != null || session.LatestFrame == null) return;
        var frame = session.LatestFrame;
        var bitmap = new WriteableBitmap(frame.Width, frame.Height, 96, 96, PixelFormats.Bgra32, null);
        bitmap.WritePixels(new Int32Rect(0, 0, frame.Width, frame.Height), frame.BgraPixels, frame.Stride, 0);
        session.PreviewBitmap = bitmap;
        PreviewBitmapChanged?.Invoke(session.DeviceId, bitmap);
    }

    private void CopyFrame(CameraSession session)
    {
        var bitmap = session.PreviewBitmap;
        var frame = session.LatestFrame;
        if (bitmap == null || frame == null) return;
        if (frame.Width != bitmap.PixelWidth || frame.Height != bitmap.PixelHeight) return;
        bitmap.WritePixels(new Int32Rect(0, 0, frame.Width, frame.Height), frame.BgraPixels, frame.Stride, 0);
    }
}
