using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using uTank2;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using Decal.Filters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UBHelper;

namespace MosswartMassacre
{
    /// <summary>
    /// Cross-machine replacement for UB's Networking + VTankFellowHeals. Shares
    /// vitals, positions, items, and debuff cast coordination between characters
    /// via MosswartOverlord's WebSocket pipeline. Received messages are queued
    /// onto the game main thread and fed into UBHelper.vTank.Instance so that
    /// VTank's internal state matches what local UB sharing would produce.
    ///
    /// Activation is opt-in via PluginSettings.VitalSharingEnabled. Default off.
    ///
    /// Runs on System.Windows.Forms.Timer (UI thread) for DECAL COM safety.
    /// </summary>
    public class VitalSharingTracker : IDisposable
    {
        private const int VitalIntervalMs = 150;
        private const int PositionIntervalMs = 300;
        private const int ItemIntervalMs = 5000;

        private static readonly Regex ProcCastRegex = new Regex(
            @"^You cast (?<spellName>.+) on .+\.?$",
            RegexOptions.Compiled);

        private readonly IPluginLogger _logger;
        private readonly Queue<Action> _networkActionQueue = new Queue<Action>();
        private readonly object _queueLock = new object();

        private System.Windows.Forms.Timer _timer;
        private bool _active;
        private bool _disposed;

        // Last-sent dedup state (vitals/position/items only send on change)
        private int _lastHealth = -1, _lastMaxHealth = -1;
        private int _lastStamina = -1, _lastMaxStamina = -1;
        private int _lastMana = -1, _lastMaxMana = -1;
        private double _lastEW, _lastNS, _lastZ;
        private double _lastHeading;
        private DateTime _lastVitalSendUtc = DateTime.MinValue;
        private DateTime _lastPositionSendUtc = DateTime.MinValue;
        private DateTime _lastItemSendUtc = DateTime.MinValue;

        // Cast attempt dedup — UB uses 1200ms to avoid spamming repeat casts
        private int _lastAttemptedSpellId;
        private int _lastAttemptedTarget;
        private DateTime _lastCastAttempt = DateTime.MinValue;
        private int _lastCastSuccessSpellId;
        private DateTime _lastCastSuccess = DateTime.MinValue;

        public bool IsActive => _active;

        // ── Peer snapshot state (used by the in-game overlay) ──
        public class PeerSnapshot
        {
            public string CharacterName;
            public int CurrentHealth, MaxHealth;
            public int CurrentStamina, MaxStamina;
            public int CurrentMana, MaxMana;
            // Position of the peer in world coordinates (from share_position_update)
            public double EW, NS, Z;
            public double Heading;
            public bool HasPosition;
            public DateTime LastUpdate;
        }

        private static readonly Dictionary<string, PeerSnapshot> _peers =
            new Dictionary<string, PeerSnapshot>(StringComparer.OrdinalIgnoreCase);
        private static readonly object _peersLock = new object();

        public static PeerSnapshot[] GetPeerSnapshots()
        {
            lock (_peersLock)
            {
                var arr = new PeerSnapshot[_peers.Count];
                _peers.Values.CopyTo(arr, 0);
                return arr;
            }
        }

        /// <summary>
        /// Snapshot for the local character (first row in the overlay so you
        /// always see yourself even before any peer traffic arrives).
        /// </summary>
        public static PeerSnapshot GetSelfSnapshot()
        {
            try
            {
                var actions = CoreManager.Current.Actions;
                return new PeerSnapshot
                {
                    CharacterName = CoreManager.Current.CharacterFilter?.Name ?? "",
                    CurrentHealth = actions.Vital[VitalType.CurrentHealth],
                    MaxHealth = actions.Vital[VitalType.MaximumHealth],
                    CurrentStamina = actions.Vital[VitalType.CurrentStamina],
                    MaxStamina = actions.Vital[VitalType.MaximumStamina],
                    CurrentMana = actions.Vital[VitalType.CurrentMana],
                    MaxMana = actions.Vital[VitalType.MaximumMana],
                    LastUpdate = DateTime.UtcNow,
                };
            }
            catch { return null; }
        }

        public VitalSharingTracker(IPluginLogger logger)
        {
            _logger = logger;
        }

        public void Start()
        {
            if (_active) return;
            _active = true;

            // Subscribe to packet and chat hooks on the main thread
            try
            {
                CoreManager.Current.EchoFilter.ClientDispatch += EchoFilter_ClientDispatch;
                CoreManager.Current.ChatBoxMessage += Core_ChatBoxMessage;
                CoreManager.Current.RenderFrame += Core_RenderFrame;
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] Hook subscribe error: {ex.Message}");
            }

            _timer = new System.Windows.Forms.Timer();
            _timer.Interval = VitalIntervalMs; // tick at vital cadence, other intervals gated internally
            _timer.Tick += OnTick;
            _timer.Start();

            // Announce to backend that we're opted in so it knows to forward shared events to us
            _ = WebSocket.SendVitalShareAsync(new
            {
                type = "share_subscribe",
                timestamp = DateTime.UtcNow.ToString("o"),
                character_name = SafeCharacterName(),
                player_id = SafePlayerId(),
                tags = GetTagsArray(),
            });

            _logger?.Log("[VitalShare] Started vital sharing tracker");
        }

        public void Stop()
        {
            if (!_active) return;
            _active = false;

            try
            {
                CoreManager.Current.EchoFilter.ClientDispatch -= EchoFilter_ClientDispatch;
                CoreManager.Current.ChatBoxMessage -= Core_ChatBoxMessage;
                CoreManager.Current.RenderFrame -= Core_RenderFrame;
            }
            catch { }

            if (_timer != null)
            {
                _timer.Stop();
                _timer.Tick -= OnTick;
                _timer.Dispose();
                _timer = null;
            }

            // Tell backend to stop forwarding shared events to us
            try
            {
                _ = WebSocket.SendVitalShareAsync(new
                {
                    type = "share_unsubscribe",
                    timestamp = DateTime.UtcNow.ToString("o"),
                    character_name = SafeCharacterName(),
                });
            }
            catch { }

            _logger?.Log("[VitalShare] Stopped vital sharing tracker");
        }

        // Called by PluginCore when a server command arrives that looks like a share_* message.
        // Runs on UI thread via the command queue.
        public void HandleIncomingShareMessage(string rawJson)
        {
            if (!_active) return;
            try
            {
                var root = JObject.Parse(rawJson);
                var type = (string)root["type"];
                if (string.IsNullOrEmpty(type)) return;

                switch (type)
                {
                    case "share_vital_update":
                        HandleShareVitalUpdate(root);
                        break;
                    case "share_cast_attempt":
                        HandleShareCastAttempt(root);
                        break;
                    case "share_cast_success":
                        HandleShareCastSuccess(root);
                        break;
                    case "share_position_update":
                        HandleSharePositionUpdate(root);
                        break;
                    case "share_item_update":
                        // Dashboard informational only. No VTank feeding.
                        break;
                }
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] HandleIncomingShareMessage error: {ex.Message}");
            }
        }

        private void HandleShareVitalUpdate(JObject root)
        {
            // Skip if the update is for our own character (backend should filter but be safe)
            string fromChar = (string)root["character_name"] ?? "";
            if (fromChar.Equals(SafeCharacterName(), StringComparison.OrdinalIgnoreCase))
                return;

            int playerId = (int?)root["player_id"] ?? 0;
            int curH = (int?)root["current_health"] ?? 0;
            int maxH = (int?)root["max_health"] ?? 0;
            int curS = (int?)root["current_stamina"] ?? 0;
            int maxS = (int?)root["max_stamina"] ?? 0;
            int curM = (int?)root["current_mana"] ?? 0;
            int maxM = (int?)root["max_mana"] ?? 0;
            if (playerId == 0 || maxH == 0) return;

            // Cache for overlay view
            lock (_peersLock)
            {
                if (!_peers.TryGetValue(fromChar, out var snap))
                {
                    snap = new PeerSnapshot { CharacterName = fromChar };
                    _peers[fromChar] = snap;
                }
                snap.CurrentHealth = curH; snap.MaxHealth = maxH;
                snap.CurrentStamina = curS; snap.MaxStamina = maxS;
                snap.CurrentMana = curM; snap.MaxMana = maxM;
                snap.LastUpdate = DateTime.UtcNow;
            }

            Enqueue(() =>
            {
                try
                {
                    if (vTank.Instance == null)
                    {
                        VerboseLog($"[VitalShare→VTank] SKIP HelperPlayerUpdate ({fromChar}): vTank.Instance is null");
                        return;
                    }
                    var info = new sPlayerInfoUpdate
                    {
                        PlayerID = playerId,
                        HasHealthInfo = true,
                        HasManaInfo = true,
                        HasStamInfo = true,
                        curHealth = curH,
                        curMana = curM,
                        curStam = curS,
                        maxHealth = maxH,
                        maxMana = maxM,
                        maxStam = maxS,
                    };
                    vTank.Instance.HelperPlayerUpdate(info);
                    VerboseLog($"[VitalShare→VTank] HelperPlayerUpdate {fromChar} (0x{playerId:X8}) HP={curH}/{maxH} S={curS}/{maxS} M={curM}/{maxM}");
                }
                catch (Exception ex) { _logger?.Log($"[VitalShare] HelperPlayerUpdate error: {ex.Message}"); }
            });
        }

        private void HandleSharePositionUpdate(JObject root)
        {
            string fromChar = (string)root["character_name"] ?? "";
            if (string.IsNullOrEmpty(fromChar)) return;
            if (fromChar.Equals(SafeCharacterName(), StringComparison.OrdinalIgnoreCase))
                return;

            double ew = (double?)root["ew"] ?? 0;
            double ns = (double?)root["ns"] ?? 0;
            double z = (double?)root["z"] ?? 0;
            double heading = (double?)root["heading"] ?? 0;

            lock (_peersLock)
            {
                if (!_peers.TryGetValue(fromChar, out var snap))
                {
                    snap = new PeerSnapshot { CharacterName = fromChar };
                    _peers[fromChar] = snap;
                }
                snap.EW = ew; snap.NS = ns; snap.Z = z;
                snap.Heading = heading;
                snap.HasPosition = true;
                snap.LastUpdate = DateTime.UtcNow;
            }
        }

        private void HandleShareCastAttempt(JObject root)
        {
            string fromChar = (string)root["character_name"] ?? "";
            if (fromChar.Equals(SafeCharacterName(), StringComparison.OrdinalIgnoreCase))
                return;

            int spellId = (int?)root["spell_id"] ?? 0;
            int target = (int?)root["target_id"] ?? 0;
            double skill = (double?)root["skill"] ?? 0;
            if (spellId == 0 || target == 0) return;

            Enqueue(() =>
            {
                try
                {
                    if (vTank.Instance == null)
                    {
                        VerboseLog($"[VitalShare→VTank] SKIP LogCastAttempt (from {fromChar}): vTank.Instance is null");
                        return;
                    }
                    if (!CoreManager.Current.Actions.IsValidObject(target))
                    {
                        VerboseLog($"[VitalShare→VTank] SKIP LogCastAttempt (from {fromChar}): target 0x{target:X8} not a valid object locally");
                        return;
                    }
                    vTank.Instance.LogCastAttempt(spellId, target, (int)skill);
                    VerboseLog($"[VitalShare→VTank] LogCastAttempt from {fromChar}: spell={spellId} target=0x{target:X8} skill={(int)skill}");
                }
                catch (Exception ex) { _logger?.Log($"[VitalShare] LogCastAttempt error: {ex.Message}"); }
            });
        }

        private void HandleShareCastSuccess(JObject root)
        {
            string fromChar = (string)root["character_name"] ?? "";
            if (fromChar.Equals(SafeCharacterName(), StringComparison.OrdinalIgnoreCase))
                return;

            int spellId = (int?)root["spell_id"] ?? 0;
            int target = (int?)root["target_id"] ?? 0;
            int duration = (int?)root["duration_ms"] ?? 0;
            if (spellId == 0 || target == 0) return;

            Enqueue(() =>
            {
                try
                {
                    if (vTank.Instance == null)
                    {
                        VerboseLog($"[VitalShare→VTank] SKIP LogSpellCast (from {fromChar}): vTank.Instance is null");
                        return;
                    }
                    if (!CoreManager.Current.Actions.IsValidObject(target))
                    {
                        VerboseLog($"[VitalShare→VTank] SKIP LogSpellCast (from {fromChar}): target 0x{target:X8} not a valid object locally");
                        return;
                    }
                    vTank.Instance.LogSpellCast(target, spellId, duration);
                    VerboseLog($"[VitalShare→VTank] LogSpellCast from {fromChar}: spell={spellId} target=0x{target:X8} duration={duration}ms");
                }
                catch (Exception ex) { _logger?.Log($"[VitalShare] LogSpellCast error: {ex.Message}"); }
            });
        }

        private void Enqueue(Action action)
        {
            lock (_queueLock)
            {
                _networkActionQueue.Enqueue(action);
            }
        }

        private void Core_RenderFrame(object sender, EventArgs e)
        {
            if (!_active) return;
            // Drain queued VTank API calls on the main thread
            while (true)
            {
                Action next = null;
                lock (_queueLock)
                {
                    if (_networkActionQueue.Count == 0) break;
                    next = _networkActionQueue.Dequeue();
                }
                try { next?.Invoke(); }
                catch (Exception ex) { _logger?.Log($"[VitalShare] Queued action error: {ex.Message}"); }
            }
        }

        private void OnTick(object sender, EventArgs e)
        {
            if (!_active) return;
            try
            {
                // Send vitals on every tick (150ms) if changed
                TrySendVitalUpdate(false);

                var now = DateTime.UtcNow;
                if (now - _lastPositionSendUtc >= TimeSpan.FromMilliseconds(PositionIntervalMs))
                    TrySendPositionUpdate(false);
                if (now - _lastItemSendUtc >= TimeSpan.FromMilliseconds(ItemIntervalMs))
                    TrySendItemUpdate(false);
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] OnTick error: {ex.Message}");
            }
        }

        private void TrySendVitalUpdate(bool force)
        {
            try
            {
                int curH = CoreManager.Current.Actions.Vital[VitalType.CurrentHealth];
                int maxH = CoreManager.Current.Actions.Vital[VitalType.MaximumHealth];
                int curS = CoreManager.Current.Actions.Vital[VitalType.CurrentStamina];
                int maxS = CoreManager.Current.Actions.Vital[VitalType.MaximumStamina];
                int curM = CoreManager.Current.Actions.Vital[VitalType.CurrentMana];
                int maxM = CoreManager.Current.Actions.Vital[VitalType.MaximumMana];

                bool changed = curH != _lastHealth || maxH != _lastMaxHealth
                             || curS != _lastStamina || maxS != _lastMaxStamina
                             || curM != _lastMana || maxM != _lastMaxMana;
                // Heartbeat every 3s even when unchanged so late subscribers see something
                bool heartbeat = (DateTime.UtcNow - _lastVitalSendUtc) > TimeSpan.FromSeconds(3);
                if (!force && !changed && !heartbeat) return;

                _lastHealth = curH; _lastMaxHealth = maxH;
                _lastStamina = curS; _lastMaxStamina = maxS;
                _lastMana = curM; _lastMaxMana = maxM;
                _lastVitalSendUtc = DateTime.UtcNow;

                var payload = new
                {
                    type = "share_vital_update",
                    timestamp = DateTime.UtcNow.ToString("o"),
                    character_name = SafeCharacterName(),
                    player_id = SafePlayerId(),
                    tags = GetTagsArray(),
                    current_health = curH,
                    max_health = maxH,
                    current_stamina = curS,
                    max_stamina = maxS,
                    current_mana = curM,
                    max_mana = maxM,
                };
                _ = WebSocket.SendVitalShareAsync(payload);
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] TrySendVitalUpdate error: {ex.Message}");
            }
        }

        private void TrySendPositionUpdate(bool force)
        {
            try
            {
                var me = Coordinates.Me;
                double heading = CoreManager.Current.Actions.Heading;

                bool changed = Math.Abs(me.EW - _lastEW) > 0.00001
                             || Math.Abs(me.NS - _lastNS) > 0.00001
                             || Math.Abs(me.Z - _lastZ) > 0.1
                             || Math.Abs(heading - _lastHeading) > 0.5;
                bool heartbeat = (DateTime.UtcNow - _lastPositionSendUtc) > TimeSpan.FromSeconds(10);
                if (!force && !changed && !heartbeat) return;

                _lastEW = me.EW; _lastNS = me.NS; _lastZ = me.Z; _lastHeading = heading;
                _lastPositionSendUtc = DateTime.UtcNow;

                var payload = new
                {
                    type = "share_position_update",
                    timestamp = DateTime.UtcNow.ToString("o"),
                    character_name = SafeCharacterName(),
                    player_id = SafePlayerId(),
                    tags = GetTagsArray(),
                    ew = Math.Round(me.EW, 7),
                    ns = Math.Round(me.NS, 7),
                    z = Math.Round(me.Z, 2),
                    heading = Math.Round(heading, 1),
                };
                _ = WebSocket.SendVitalShareAsync(payload);
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] TrySendPositionUpdate error: {ex.Message}");
            }
        }

        private void TrySendItemUpdate(bool force)
        {
            // Placeholder — item count tracking mirrors UB's TrackedItemUpdateMessage.
            // For now we only send a heartbeat-style update so the dashboard knows we're alive.
            try
            {
                if (!force && (DateTime.UtcNow - _lastItemSendUtc) < TimeSpan.FromSeconds(ItemIntervalMs / 1000.0))
                    return;
                _lastItemSendUtc = DateTime.UtcNow;

                var payload = new
                {
                    type = "share_item_update",
                    timestamp = DateTime.UtcNow.ToString("o"),
                    character_name = SafeCharacterName(),
                    player_id = SafePlayerId(),
                    tags = GetTagsArray(),
                };
                _ = WebSocket.SendVitalShareAsync(payload);
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] TrySendItemUpdate error: {ex.Message}");
            }
        }

        private void EchoFilter_ClientDispatch(object sender, NetworkMessageEventArgs e)
        {
            if (!_active) return;
            try
            {
                // Magic_CastTargetedSpell (0xF7B1 action 0x004A) — outgoing spell cast packet
                if (e.Message.Type != 0xF7B1) return;
                int action = e.Message.Value<int>("action");
                if (action != 0x004A) return;

                int target = e.Message.Value<int>("target");
                int spellId = e.Message.Value<int>("spell");

                // 1200ms dedup (same as UB)
                if (_lastAttemptedSpellId == spellId && _lastAttemptedTarget == target
                    && (DateTime.UtcNow - _lastCastAttempt) < TimeSpan.FromMilliseconds(1200))
                    return;

                if (!IsDebuffSpell(spellId)) return;

                double skill = GetEffectiveSkillForSpell(spellId);

                _lastCastAttempt = DateTime.UtcNow;
                _lastAttemptedSpellId = spellId;
                _lastAttemptedTarget = target;

                var payload = new
                {
                    type = "share_cast_attempt",
                    timestamp = DateTime.UtcNow.ToString("o"),
                    character_name = SafeCharacterName(),
                    player_id = SafePlayerId(),
                    tags = GetTagsArray(),
                    spell_id = spellId,
                    target_id = target,
                    skill = skill,
                };
                _ = WebSocket.SendVitalShareAsync(payload);
                VerboseLog($"[VitalShare] SEND share_cast_attempt spell={spellId} target=0x{target:X8} skill={(int)skill}");
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] EchoFilter_ClientDispatch error: {ex.Message}");
            }
        }

        private void Core_ChatBoxMessage(object sender, ChatTextInterceptEventArgs e)
        {
            if (!_active) return;
            try
            {
                if (!e.Text.StartsWith("You cast ")) return;

                int spellId;
                int target;

                if (_lastAttemptedSpellId != 0)
                {
                    // Normal cast — we already know what was cast from the outgoing packet
                    if (_lastCastSuccessSpellId == _lastAttemptedSpellId
                        && (DateTime.UtcNow - _lastCastSuccess) < TimeSpan.FromMilliseconds(50))
                        return;

                    if (!IsDebuffSpell(_lastAttemptedSpellId)) return;

                    spellId = _lastAttemptedSpellId;
                    target = _lastAttemptedTarget;
                }
                else
                {
                    // Proc debuff — parse spell name from chat
                    var match = ProcCastRegex.Match(e.Text);
                    if (!match.Success) return;
                    var parsedName = match.Groups["spellName"].Value;
                    spellId = GetSpellIdByName(parsedName);
                    if (spellId == 0) return;
                    if (!IsDebuffSpell(spellId)) return;
                    if (_lastCastSuccessSpellId == spellId
                        && (DateTime.UtcNow - _lastCastSuccess) < TimeSpan.FromMilliseconds(50))
                        return;
                    target = CoreManager.Current.Actions.CurrentSelection;
                    if (target == 0) return;
                }

                _lastCastSuccess = DateTime.UtcNow;
                _lastCastSuccessSpellId = spellId;
                int durationMs = GetSpellDurationMs(spellId);

                var payload = new
                {
                    type = "share_cast_success",
                    timestamp = DateTime.UtcNow.ToString("o"),
                    character_name = SafeCharacterName(),
                    player_id = SafePlayerId(),
                    tags = GetTagsArray(),
                    spell_id = spellId,
                    target_id = target,
                    duration_ms = durationMs,
                };
                _ = WebSocket.SendVitalShareAsync(payload);
                VerboseLog($"[VitalShare] SEND share_cast_success spell={spellId} target=0x{target:X8} duration={durationMs}ms");
            }
            catch (Exception ex)
            {
                _logger?.Log($"[VitalShare] Core_ChatBoxMessage error: {ex.Message}");
            }
        }

        // --- Spell helpers using DECAL's FileService.SpellTable ---

        private static readonly string[] DebuffNameTokens = new[]
        {
            "Vulnerability", "Imperil", "Succumb",
            "Weakness", "Feeblemind", "Dispirit", "Impotence",
            "Slowness", "Clumsiness", "Frailty", "Bafflement",
            "Tranquility",
            // Elemental vulns
            "Fire Vulnerability", "Cold Vulnerability",
            "Acid Vulnerability", "Lightning Vulnerability",
            "Blade Vulnerability", "Pierce Vulnerability", "Bludgeon Vulnerability",
            // Defense debuffs
            "Defenselessness", "Open Mind",
        };

        private bool IsDebuffSpell(int spellId)
        {
            try
            {
                var spell = CoreManager.Current.Filter<FileService>()?.SpellTable?.GetById(spellId);
                if (spell == null) return false;
                string name = spell.Name ?? "";
                foreach (var token in DebuffNameTokens)
                {
                    if (name.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0)
                        return true;
                }
                return false;
            }
            catch { return false; }
        }

        // Cache of spell name → id, built lazily on first use
        private static Dictionary<string, int> _spellNameCache;

        private int GetSpellIdByName(string name)
        {
            try
            {
                if (_spellNameCache == null)
                {
                    _spellNameCache = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
                    var fs = CoreManager.Current.Filter<FileService>();
                    if (fs?.SpellTable == null) return 0;
                    // AC spell IDs cluster in a few ranges — scan up to 10000
                    for (int id = 1; id < 10000; id++)
                    {
                        try
                        {
                            var s = fs.SpellTable.GetById(id);
                            if (s != null && !string.IsNullOrEmpty(s.Name) && !_spellNameCache.ContainsKey(s.Name))
                                _spellNameCache[s.Name] = id;
                        }
                        catch { }
                    }
                }
                return _spellNameCache.TryGetValue(name, out var result) ? result : 0;
            }
            catch { }
            return 0;
        }

        private int GetSpellDurationMs(int spellId)
        {
            try
            {
                var spell = CoreManager.Current.Filter<FileService>()?.SpellTable?.GetById(spellId);
                if (spell == null) return 0;
                // DECAL's Spell.Duration is in seconds as a double — convert to ms
                return (int)(spell.Duration * 1000);
            }
            catch { return 0; }
        }

        private double GetEffectiveSkillForSpell(int spellId)
        {
            try
            {
                var spell = CoreManager.Current.Filter<FileService>()?.SpellTable?.GetById(spellId);
                if (spell == null) return 0;
                var cf = CoreManager.Current.CharacterFilter;
                // DECAL's Spell.School is the SpellSchool enum. Compare by ToString so we don't
                // depend on specific numeric values.
                switch (spell.School.ToString())
                {
                    case "CreatureEnchantment":
                        return cf.EffectiveSkill[CharFilterSkillType.CreatureEnchantment];
                    case "ItemEnchantment":
                        return cf.EffectiveSkill[CharFilterSkillType.ItemEnchantment];
                    case "LifeMagic":
                        return cf.EffectiveSkill[CharFilterSkillType.LifeMagic];
                    case "WarMagic":
                        return cf.EffectiveSkill[CharFilterSkillType.WarMagic];
                    case "VoidMagic":
                        return cf.EffectiveSkill[CharFilterSkillType.VoidMagic];
                    default:
                        return cf.EffectiveSkill[CharFilterSkillType.ItemEnchantment];
                }
            }
            catch { return 0; }
        }

        // --- Helpers ---

        /// <summary>
        /// Logs a message routed to the plugin chat window, but ONLY when
        /// PluginSettings.VerboseLogging is true. Used to trace every
        /// VTank feeding event (HelperPlayerUpdate / LogCastAttempt /
        /// LogSpellCast) so the user can verify cross-PC coordination is
        /// actually reaching VTank.
        /// </summary>
        private void VerboseLog(string message)
        {
            try
            {
                if (!PluginSettings.IsInitialized) return;
                if (!PluginSettings.Instance.VerboseLogging) return;
                PluginCore.WriteToChat(message);
            }
            catch { }
        }

        private string SafeCharacterName()
        {
            try { return CoreManager.Current.CharacterFilter.Name ?? ""; }
            catch { return ""; }
        }

        private int SafePlayerId()
        {
            try { return CoreManager.Current.CharacterFilter.Id; }
            catch { return 0; }
        }

        private string[] GetTagsArray()
        {
            try
            {
                // Reuse the existing CharTag setting as a single-value tag
                var tag = PluginSettings.Instance.CharTag;
                if (string.IsNullOrWhiteSpace(tag)) return new string[0];
                return new[] { tag };
            }
            catch { return new string[0]; }
        }

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