using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Threading;
using MarcusLaw.OutlookAddin.UI.ViewModels;
using Serilog;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace OutlookAddin.Services
{
    /// <summary>
    /// Subscribes to <see cref="Outlook.Explorer.SelectionChange"/>, debounces
    /// the storm of events Outlook fires while the user is scrolling
    /// (typically every ~30ms), and forwards the resulting sender-email
    /// lookup to the <see cref="ReadingPaneViewModel"/>.
    /// </summary>
    public sealed class ExplorerSelectionHandler : IDisposable
    {
        public TimeSpan DebounceInterval { get; }

        private readonly Outlook.Explorer _explorer;
        private readonly ReadingPaneViewModel _viewModel;
        private readonly Dispatcher _dispatcher;
        private readonly ILogger _logger;
        private readonly Timer _timer;
        private string? _lastSenderEmail;
        private int _disposed;

        public ExplorerSelectionHandler(
            Outlook.Explorer explorer,
            ReadingPaneViewModel viewModel,
            Dispatcher dispatcher,
            ILogger logger,
            TimeSpan? debounce = null)
        {
            _explorer = explorer ?? throw new ArgumentNullException(nameof(explorer));
            _viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
            _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            DebounceInterval = debounce ?? TimeSpan.FromMilliseconds(200);

            _timer = new Timer(OnTimerFired, null, Timeout.Infinite, Timeout.Infinite);
            _explorer.SelectionChange += OnSelectionChange;
        }

        public void TriggerNow()
        {
            OnSelectionChange();
        }

        private void OnSelectionChange()
        {
            if (Volatile.Read(ref _disposed) != 0) return;
            try
            {
                _timer.Change(DebounceInterval, Timeout.InfiniteTimeSpan);
            }
            catch (ObjectDisposedException) { }
        }

        private void OnTimerFired(object? state)
        {
            if (Volatile.Read(ref _disposed) != 0) return;
            try
            {
                _dispatcher.BeginInvoke((Action)ProcessOnUiThread);
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "ExplorerSelectionHandler: dispatcher invoke failed");
            }
        }

        private void ProcessOnUiThread()
        {
            string? senderEmail = ReadSelectedSenderEmail();

            // Only kick a lookup when the sender has actually changed; otherwise
            // we'd hammer the cache for every keyboard arrow press on the same row.
            if (string.Equals(senderEmail, _lastSenderEmail, StringComparison.OrdinalIgnoreCase)) return;
            _lastSenderEmail = senderEmail;

            if (string.IsNullOrWhiteSpace(senderEmail))
            {
                _viewModel.Reset();
                return;
            }

            _ = _viewModel.LoadAsync(senderEmail);
        }

        private string? ReadSelectedSenderEmail()
        {
            Outlook.Selection? selection = null;
            object? entry = null;
            try
            {
                selection = _explorer.Selection;
                if (selection == null || selection.Count == 0) return null;
                entry = selection[1];
                if (entry is Outlook.MailItem mail)
                {
                    var smtp = SafeGet(() => mail.SenderEmailAddress);
                    if (!string.IsNullOrWhiteSpace(smtp) && smtp.Contains("@"))
                    {
                        return smtp;
                    }
                }
                return null;
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "ExplorerSelectionHandler: ReadSelectedSenderEmail failed");
                return null;
            }
            finally
            {
                Release(entry);
                Release(selection);
            }
        }

        private static T? SafeGet<T>(Func<T> getter) where T : class
        {
            try { return getter(); } catch { return null; }
        }

        private static void Release(object? com)
        {
            if (com != null && Marshal.IsComObject(com))
            {
                try { Marshal.ReleaseComObject(com); } catch { /* ignore */ }
            }
        }

        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
            try { _explorer.SelectionChange -= OnSelectionChange; } catch { /* ignore */ }
            try { _timer.Dispose(); } catch { /* ignore */ }
        }
    }
}
