using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;
using ArchestrA.MxAccess;

namespace MxAccess.Cli.Mx
{
    /// Wraps the MxAccess COM proxy with caller-friendly primitives.
    ///
    /// MxAccess events are dispatched as COM messages on the apartment that
    /// called Register. Pure Monitor / AutoResetEvent waits do *not* drain
    /// those messages reliably even on STA, so MxSession exposes a
    /// `WaitForUpdate` that polls Application.DoEvents() instead. This is the
    /// pattern Object Viewer / WindowViewer / aaTagViewer all use under the
    /// hood — see docs/api-notes.md "Threading model".
    public sealed class MxSession : IDisposable
    {
        private readonly LMXProxyServerClass _proxy;
        private readonly int _hServer;
        private readonly object _lock = new object();
        private readonly Dictionary<int, MxItem> _itemsByHandle = new Dictionary<int, MxItem>();
        private readonly ConcurrentQueue<MxUpdate> _updates = new ConcurrentQueue<MxUpdate>();
        private bool _disposed;

        public MxSession(string clientName)
        {
            _proxy = new LMXProxyServerClass();
            _proxy.OnDataChange      += OnDataChange;
            _proxy.OnWriteComplete   += OnWriteComplete;
            _proxy.OperationComplete += OnOperationComplete;
            _hServer = _proxy.Register(string.IsNullOrWhiteSpace(clientName) ? "mxa" : clientName);
        }

        public int ServerHandle => _hServer;

        public MxItem AddItem(string itemRef)
        {
            if (string.IsNullOrWhiteSpace(itemRef))
                throw new ArgumentException("Item reference must be non-empty.", nameof(itemRef));
            var hItem = _proxy.AddItem(_hServer, itemRef);
            var item = new MxItem(this, _proxy, _hServer, hItem, itemRef);
            lock (_lock) _itemsByHandle[hItem] = item;
            return item;
        }

        /// Authenticate a user against the galaxy and return the user id
        /// to pass to subsequent Write/WriteSecured calls. Returns 0 on
        /// failure (bad credentials, unknown user, galaxy auth disabled).
        ///
        /// `verifyUser` is the credential string the proxy expects for its
        /// configured galaxy authentication mode:
        ///   - osAuthenticationMode      → `<domain-or-host>\<username>`
        ///   - galaxyAuthenticationMode  → `<username>` only
        ///   - mixed/AAD                 → `<UPN>` (e.g. `user@example.com`)
        public int Authenticate(string verifyUser, string password)
        {
            if (string.IsNullOrEmpty(verifyUser))
                throw new ArgumentException("verifyUser must be non-empty.", nameof(verifyUser));
            // Some galaxy configurations (e.g. eOSUserBased) cause the proxy to
            // throw `ArgumentException: Value does not fall within the expected
            // range` for bad credentials instead of returning 0 like the
            // permissive (eNone) configuration does. Both shapes mean "auth
            // failed"; the caller distinguishes via a non-zero return value.
            try
            {
                return _proxy.AuthenticateUser(_hServer, verifyUser, password ?? string.Empty);
            }
            catch (ArgumentException)
            {
                return 0;
            }
        }

        /// Pump COM messages while watching for an update that matches the predicate.
        /// Returns true when one is captured, false on timeout.
        public bool WaitForUpdate(Predicate<MxUpdate> match, TimeSpan timeout, out MxUpdate captured)
        {
            captured = null;
            var deadline = DateTime.UtcNow + timeout;
            while (DateTime.UtcNow < deadline)
            {
                Application.DoEvents();
                while (_updates.TryDequeue(out var u))
                {
                    if (match(u)) { captured = u; return true; }
                }
                Thread.Sleep(20);
            }
            // One last drain after the deadline so we don't miss an event that arrived
            // between the final Sleep and the loop exit.
            Application.DoEvents();
            while (_updates.TryDequeue(out var u))
            {
                if (match(u)) { captured = u; return true; }
            }
            return false;
        }

        /// Drain all pending updates without blocking. Caller must call PumpOnce()
        /// in their own loop to keep the COM message queue moving.
        public IEnumerable<MxUpdate> DrainUpdates()
        {
            while (_updates.TryDequeue(out var u)) yield return u;
        }

        /// Pump COM messages once. Used by streaming subscribers between drains.
        public void PumpOnce(TimeSpan slice)
        {
            Application.DoEvents();
            if (slice > TimeSpan.Zero) Thread.Sleep(slice);
        }

        internal void RemoveItem(int hItem)
        {
            lock (_lock) _itemsByHandle.Remove(hItem);
        }

        // ---- Event plumbing ----

        private void OnDataChange(int hServer, int hItem, object value, int quality, object timestamp,
            ref MXSTATUS_PROXY[] vars)
        {
            string itemRef;
            lock (_lock) itemRef = _itemsByHandle.TryGetValue(hItem, out var it) ? it.Reference : null;
            _updates.Enqueue(new MxUpdate
            {
                Kind          = MxUpdateKind.DataChange,
                ItemHandle    = hItem,
                ItemReference = itemRef,
                Value         = value,
                Quality       = quality,
                Timestamp     = TryFiletimeToDateTime(timestamp),
                Statuses      = MxStatusInfo.From(vars),
            });
        }

        private void OnWriteComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] vars)
        {
            string itemRef;
            lock (_lock) itemRef = _itemsByHandle.TryGetValue(hItem, out var it) ? it.Reference : null;
            _updates.Enqueue(new MxUpdate
            {
                Kind          = MxUpdateKind.WriteComplete,
                ItemHandle    = hItem,
                ItemReference = itemRef,
                Statuses      = MxStatusInfo.From(vars),
            });
        }

        private void OnOperationComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] vars)
        {
            string itemRef;
            lock (_lock) itemRef = _itemsByHandle.TryGetValue(hItem, out var it) ? it.Reference : null;
            _updates.Enqueue(new MxUpdate
            {
                Kind          = MxUpdateKind.OperationComplete,
                ItemHandle    = hItem,
                ItemReference = itemRef,
                Statuses      = MxStatusInfo.From(vars),
            });
        }

        private static DateTime? TryFiletimeToDateTime(object ft)
        {
            if (ft == null) return null;
            try
            {
                var asLong = Convert.ToInt64(ft);
                return DateTime.FromFileTimeUtc(asLong).ToLocalTime();
            }
            catch { return null; }
        }

        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;

            MxItem[] items;
            lock (_lock)
            {
                items = new MxItem[_itemsByHandle.Count];
                _itemsByHandle.Values.CopyTo(items, 0);
                _itemsByHandle.Clear();
            }
            foreach (var it in items)
            {
                try { it.Dispose(); } catch { }
            }
            try { _proxy.Unregister(_hServer); } catch { }
        }
    }
}
