using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using MarcusLaw.OutlookAddin.Core.Models;
using Serilog;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace OutlookAddin.Services
{
    /// <summary>
    /// Watches the user's default Sent Items folder for newly added mail
    /// that carries our <c>OutlookAddinGuid</c> UserProperty. When a match
    /// arrives, fires <see cref="MailFiled"/> with the resolved
    /// <see cref="MailEnvelope"/> + target so the host can POST it to
    /// EspoCRM.
    ///
    /// The MAPI provider (Outlook) only fires Items.ItemAdd while the
    /// reference stays rooted, so we keep <see cref="_sentItems"/> in a
    /// field — letting it be GC'd silently drops the subscription.
    /// </summary>
    public sealed class SentItemsWatcher : IDisposable
    {
        private readonly Outlook.Application _app;
        private readonly ILogger _logger;
        private readonly Dictionary<string, PendingItem> _pending = new Dictionary<string, PendingItem>();
        private readonly Timer _expiryTimer;

        private Outlook.Items? _sentItems;
        private int _disposed;

        public TimeSpan PendingTimeout { get; } = TimeSpan.FromMinutes(2);

        public event EventHandler<SentMailMatchedEventArgs>? MailMatched;

        public SentItemsWatcher(Outlook.Application app, ILogger logger)
        {
            _app = app ?? throw new ArgumentNullException(nameof(app));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _expiryTimer = new Timer(_ => SweepExpired(), null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
        }

        public void Initialize()
        {
            try
            {
                var ns = _app.Session;
                var folder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail);
                _sentItems = folder.Items;
                _sentItems.ItemAdd += OnSentItemAdd;
                _logger.Information("SentItemsWatcher attached to Sent Items folder");
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "SentItemsWatcher.Initialize failed");
            }
        }

        /// <summary>
        /// Registers a guid → target mapping. When a Sent Items item with
        /// the matching UserProperty arrives, the watcher fires
        /// <see cref="MailMatched"/>.
        /// </summary>
        public void Track(string guid, FilingTarget target)
        {
            if (string.IsNullOrWhiteSpace(guid)) return;
            lock (_pending)
            {
                _pending[guid] = new PendingItem(target, DateTimeOffset.UtcNow);
            }
        }

        private void OnSentItemAdd(object item)
        {
            if (Volatile.Read(ref _disposed) != 0) return;
            try
            {
                if (item is not Outlook.MailItem mail) return;

                var guid = TryReadUserProperty(mail, "OutlookAddinGuid");
                var caseId = TryReadUserProperty(mail, "EspoCaseId");
                var caseName = TryReadUserProperty(mail, "EspoCaseName");
                if (string.IsNullOrWhiteSpace(guid)) return;

                PendingItem? pending = null;
                lock (_pending)
                {
                    if (_pending.TryGetValue(guid!, out var p))
                    {
                        pending = p;
                        _pending.Remove(guid!);
                    }
                }

                var target = pending?.Target;
                if (target == null && !string.IsNullOrWhiteSpace(caseId))
                {
                    target = new FilingTarget("Case", caseId!, caseName ?? caseId!);
                }
                if (target == null)
                {
                    _logger.Information("SentItemsWatcher: matched guid {Guid} has no target — skipping", guid);
                    return;
                }

                MailMatched?.Invoke(this, new SentMailMatchedEventArgs(mail, target));
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "SentItemsWatcher.OnSentItemAdd failed");
            }
        }

        private static string? TryReadUserProperty(Outlook.MailItem mail, string name)
        {
            Outlook.UserProperties? props = null;
            Outlook.UserProperty? prop = null;
            try
            {
                props = mail.UserProperties;
                prop = props?.Find(name);
                return prop?.Value as string;
            }
            catch { return null; }
            finally
            {
                if (prop != null && Marshal.IsComObject(prop)) Marshal.ReleaseComObject(prop);
                if (props != null && Marshal.IsComObject(props)) Marshal.ReleaseComObject(props);
            }
        }

        private void SweepExpired()
        {
            if (Volatile.Read(ref _disposed) != 0) return;
            var cutoff = DateTimeOffset.UtcNow - PendingTimeout;
            lock (_pending)
            {
                var stale = new List<string>();
                foreach (var kv in _pending)
                {
                    if (kv.Value.RegisteredAt < cutoff) stale.Add(kv.Key);
                }
                foreach (var k in stale) _pending.Remove(k);
            }
        }

        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
            try { if (_sentItems != null) _sentItems.ItemAdd -= OnSentItemAdd; } catch { /* ignore */ }
            _sentItems = null;
            try { _expiryTimer.Dispose(); } catch { /* ignore */ }
        }

        private sealed class PendingItem
        {
            public FilingTarget Target { get; }
            public DateTimeOffset RegisteredAt { get; }

            public PendingItem(FilingTarget target, DateTimeOffset at)
            {
                Target = target;
                RegisteredAt = at;
            }
        }
    }

    public sealed class SentMailMatchedEventArgs : EventArgs
    {
        public Outlook.MailItem Mail { get; }
        public FilingTarget Target { get; }

        public SentMailMatchedEventArgs(Outlook.MailItem mail, FilingTarget target)
        {
            Mail = mail;
            Target = target;
        }
    }
}
