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

namespace OutlookAddin.Services
{
    /// <summary>
    /// Subscribes to <see cref="Outlook.Items.ItemAdd"/> on each watched
    /// folder. For each newly arrived mail:
    /// 1. Dedup via a HashSet of EntryIDs (Outlook fires ItemAdd multiple
    ///    times per arrival under some Exchange flavours).
    /// 2. Look up the sender via <see cref="IMatchingService"/>.
    /// 3. If exactly one unambiguous match and the folder is in
    ///    <see cref="WatchMode.AutoFile"/>, file via <see cref="IFilingService"/>.
    /// 4. Otherwise stamp the "Needs filing" Outlook category.
    ///
    /// CRITICAL: the <c>Items</c> collections MUST stay rooted, otherwise
    /// the GC will quietly drop the COM subscription. We hold them in
    /// <c>_subscriptions</c> for the lifetime of the watcher.
    /// </summary>
    public sealed class FolderWatcher : IDisposable
    {
        private const string NeedsFilingCategory = "Needs filing";

        private readonly Outlook.Application _app;
        private readonly FolderResolver _resolver;
        private readonly IMatchingService _matching;
        private readonly IFilingService _filing;
        private readonly Dispatcher _dispatcher;
        private readonly ILogger _logger;

        private readonly List<Subscription> _subscriptions = new List<Subscription>();
        private readonly HashSet<string> _processedEntryIds = new HashSet<string>(StringComparer.Ordinal);
        private readonly object _processedLock = new object();
        private int _disposed;

        public FolderWatcher(
            Outlook.Application app,
            FolderResolver resolver,
            IMatchingService matching,
            IFilingService filing,
            Dispatcher dispatcher,
            ILogger logger)
        {
            _app = app ?? throw new ArgumentNullException(nameof(app));
            _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
            _matching = matching ?? throw new ArgumentNullException(nameof(matching));
            _filing = filing ?? throw new ArgumentNullException(nameof(filing));
            _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public void Apply(IReadOnlyList<WatchedFolder> watched)
        {
            if (Volatile.Read(ref _disposed) != 0) return;
            TearDown();

            if (watched == null || watched.Count == 0)
            {
                _logger.Information("FolderWatcher: no watched folders configured");
                return;
            }

            foreach (var spec in watched)
            {
                AttachOne(spec);
            }
        }

        private void AttachOne(WatchedFolder spec)
        {
            try
            {
                var folder = _resolver.Resolve(spec.StoreId, spec.FolderPath);
                if (folder == null)
                {
                    _logger.Warning("FolderWatcher: could not resolve {StoreId} / {Path}", spec.StoreId, spec.FolderPath);
                    return;
                }

                var items = folder.Items;
                var subscription = new Subscription(spec, folder, items);
                subscription.Handler = item => OnItemAdd(item, subscription);
                items.ItemAdd += subscription.Handler;
                _subscriptions.Add(subscription);
                _logger.Information("FolderWatcher: attached {Path} (mode={Mode}, threshold={Threshold})",
                    spec.FolderPath, spec.Mode, spec.ConfidenceThreshold);
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "FolderWatcher.AttachOne failed for {Path}", spec.FolderPath);
            }
        }

        private void OnItemAdd(object item, Subscription subscription)
        {
            if (Volatile.Read(ref _disposed) != 0) return;
            try
            {
                if (item is not Outlook.MailItem mail) return;
                var entryId = SafeGet(() => mail.EntryID);
                if (string.IsNullOrWhiteSpace(entryId)) return;

                lock (_processedLock)
                {
                    if (!_processedEntryIds.Add(entryId!)) return;
                    if (_processedEntryIds.Count > 2000)
                    {
                        // Trim cold entries occasionally.
                        _processedEntryIds.Clear();
                        _processedEntryIds.Add(entryId!);
                    }
                }

                _ = ProcessAsync(mail, subscription);
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "FolderWatcher.OnItemAdd failed");
            }
        }

        private async Task ProcessAsync(Outlook.MailItem mail, Subscription subscription)
        {
            try
            {
                var sender = SafeGet(() => mail.SenderEmailAddress);
                if (string.IsNullOrWhiteSpace(sender) || !sender!.Contains("@"))
                {
                    return;
                }

                var match = await _matching.LookupAsync(sender).ConfigureAwait(true);

                var spec = subscription.Spec;
                var threshold = spec.ConfidenceThreshold;
                var hasConfidentMatch =
                    match != null &&
                    !match.IsAmbiguous &&
                    match.Contact != null;

                var confidence = hasConfidentMatch ? 1.0 : 0.0;

                FilingTarget? target = null;
                if (hasConfidentMatch)
                {
                    target = ResolveTargetFromMatch(match!, spec);
                }
                else if (!string.IsNullOrWhiteSpace(spec.DefaultParentId) && !string.IsNullOrWhiteSpace(spec.DefaultParentType))
                {
                    target = new FilingTarget(spec.DefaultParentType!, spec.DefaultParentId!, spec.DefaultParentType!);
                }

                if (target == null || confidence < threshold || spec.Mode == WatchMode.NotifyOnly)
                {
                    StampCategoryOnUiThread(mail, NeedsFilingCategory);
                    _logger.Information("FolderWatcher: notify-only for {Sender} in {Path}", sender, spec.FolderPath);
                    return;
                }

                var envelope = MailItemExtractor.Extract(mail);
                var summary = await _filing.FileAsync(new[] { envelope }, target).ConfigureAwait(true);
                if (summary.FiledCount == 1)
                {
                    StampCategoryOnUiThread(mail, "Filed: " + target.ParentName);
                    _logger.Information("FolderWatcher: auto-filed {Sender} → {Type}/{Id}",
                        sender, target.ParentType, target.ParentId);
                }
                else if (summary.QueuedCount == 1)
                {
                    _logger.Information("FolderWatcher: auto-file queued ({Path}, sender={Sender})", spec.FolderPath, sender);
                }
                else
                {
                    StampCategoryOnUiThread(mail, NeedsFilingCategory);
                }
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "FolderWatcher.ProcessAsync failed");
            }
        }

        private static FilingTarget? ResolveTargetFromMatch(MatchResult match, WatchedFolder spec)
        {
            if (match.Contact == null) return null;

            if (match.RecentCases != null && match.RecentCases.Count > 0)
            {
                var first = match.RecentCases[0];
                return new FilingTarget("Case", first.Id, first.Name ?? first.Number ?? first.Id);
            }
            return new FilingTarget("Contact", match.Contact.Id, match.Contact.Name ?? match.Contact.EmailAddress ?? match.Contact.Id);
        }

        private void StampCategoryOnUiThread(Outlook.MailItem mail, string category)
        {
            void Stamp()
            {
                try
                {
                    var existing = mail.Categories ?? string.Empty;
                    if (existing.IndexOf(category, StringComparison.Ordinal) >= 0) return;
                    mail.Categories = string.IsNullOrEmpty(existing) ? category : existing + "; " + category;
                    mail.Save();
                }
                catch (Exception ex)
                {
                    _logger.Warning(ex, "FolderWatcher: category stamp failed");
                }
            }

            if (_dispatcher.CheckAccess()) Stamp();
            else _dispatcher.BeginInvoke((Action)Stamp);
        }

        private void TearDown()
        {
            foreach (var sub in _subscriptions)
            {
                try { if (sub.Handler != null) sub.Items.ItemAdd -= sub.Handler; } catch { /* ignore */ }
                try { if (Marshal.IsComObject(sub.Items)) Marshal.ReleaseComObject(sub.Items); } catch { /* ignore */ }
                try { if (Marshal.IsComObject(sub.Folder)) Marshal.ReleaseComObject(sub.Folder); } catch { /* ignore */ }
            }
            _subscriptions.Clear();
        }

        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
            TearDown();
        }

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

        private sealed class Subscription
        {
            public WatchedFolder Spec { get; }
            public Outlook.Folder Folder { get; }
            public Outlook.Items Items { get; }
            public Outlook.ItemsEvents_ItemAddEventHandler? Handler { get; set; }

            public Subscription(WatchedFolder spec, Outlook.Folder folder, Outlook.Items items)
            {
                Spec = spec;
                Folder = folder;
                Items = items;
            }
        }
    }
}
