using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Opc.Ua;
using Serilog;
using ZB.MOM.WW.OtOpcUa.Client.Shared;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;

namespace ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;

/// <summary>
///     ViewModel for the subscriptions panel.
/// </summary>
public partial class SubscriptionsViewModel : ObservableObject
{
    private static readonly ILogger Logger = Log.ForContext<SubscriptionsViewModel>();

    private readonly IUiDispatcher _dispatcher;
    private readonly IOpcUaClientService _service;

    /// <summary>
    ///     Last user-visible status message — set when a subscribe/unsubscribe operation fails so the
    ///     shell can surface the diagnostic instead of silently dropping the error. Cleared on success.
    /// </summary>
    [ObservableProperty] private string? _statusMessage;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(AddSubscriptionCommand))]
    [NotifyCanExecuteChangedFor(nameof(RemoveSubscriptionCommand))]
    private bool _isConnected;

    [ObservableProperty] private int _newInterval = 1000;

    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddSubscriptionCommand))]
    private string? _newNodeIdText;

    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(RemoveSubscriptionCommand))]
    private SubscriptionItemViewModel? _selectedSubscription;

    [ObservableProperty] private int _subscriptionCount;

    /// <summary>
    /// Creates the subscriptions panel view model used to manage live data subscriptions and ad hoc writes from the UI.
    /// </summary>
    /// <param name="service">The shared client service that performs subscribe, unsubscribe, read, and write operations.</param>
    /// <param name="dispatcher">Marshals data-change callbacks back onto the UI thread.</param>
    public SubscriptionsViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
    {
        _service = service;
        _dispatcher = dispatcher;
        _service.DataChanged += OnDataChanged;
    }

    /// <summary>Currently active subscriptions.</summary>
    public ObservableCollection<SubscriptionItemViewModel> ActiveSubscriptions { get; } = [];

    /// <summary>Currently selected subscriptions (for multi-select remove).</summary>
    public List<SubscriptionItemViewModel> SelectedSubscriptions { get; } = [];

    private void OnDataChanged(object? sender, DataChangedEventArgs e)
    {
        _dispatcher.Post(() =>
        {
            foreach (var item in ActiveSubscriptions)
                if (item.NodeId == e.NodeId)
                {
                    item.Value = Helpers.ValueFormatter.Format(e.Value.Value);
                    item.Status = Helpers.StatusCodeFormatter.Format(e.Value.StatusCode);
                    item.Timestamp = e.Value.SourceTimestamp.ToString("O");
                }
        });
    }

    private bool CanAddSubscription()
    {
        return IsConnected && !string.IsNullOrWhiteSpace(NewNodeIdText);
    }

    [RelayCommand(CanExecute = nameof(CanAddSubscription))]
    private async Task AddSubscriptionAsync()
    {
        if (string.IsNullOrWhiteSpace(NewNodeIdText)) return;

        var nodeIdStr = NewNodeIdText;
        var interval = NewInterval;

        try
        {
            var nodeId = NodeId.Parse(nodeIdStr);
            await _service.SubscribeAsync(nodeId, interval);

            _dispatcher.Post(() =>
            {
                ActiveSubscriptions.Add(new SubscriptionItemViewModel(nodeIdStr, interval));
                SubscriptionCount = ActiveSubscriptions.Count;
                StatusMessage = null;
            });
        }
        catch (Exception ex)
        {
            Logger.Warning(ex, "AddSubscription failed for {NodeId}", nodeIdStr);
            _dispatcher.Post(() => StatusMessage = $"Subscribe failed for {nodeIdStr}: {ex.Message}");
        }
    }

    private bool CanRemoveSubscription()
    {
        return IsConnected && (SelectedSubscriptions.Count > 0 || SelectedSubscription != null);
    }

    [RelayCommand(CanExecute = nameof(CanRemoveSubscription))]
    private async Task RemoveSubscriptionAsync()
    {
        var itemsToRemove = SelectedSubscriptions.Count > 0
            ? SelectedSubscriptions.ToList()
            : SelectedSubscription != null ? [SelectedSubscription] : [];

        if (itemsToRemove.Count == 0) return;

        foreach (var item in itemsToRemove)
        {
            try
            {
                var nodeId = NodeId.Parse(item.NodeId);
                await _service.UnsubscribeAsync(nodeId);

                _dispatcher.Post(() => ActiveSubscriptions.Remove(item));
            }
            catch (Exception ex)
            {
                Logger.Warning(ex, "Unsubscribe failed for {NodeId}", item.NodeId);
                _dispatcher.Post(() => StatusMessage = $"Unsubscribe failed for {item.NodeId}: {ex.Message}");
                // Continue with the other items in the batch.
            }
        }

        _dispatcher.Post(() => SubscriptionCount = ActiveSubscriptions.Count);
    }

    /// <summary>
    ///     Subscribes to a node by ID (used by context menu). Skips if already subscribed.
    /// </summary>
    /// <param name="nodeIdStr">The node ID to subscribe to from the browse tree or persisted settings.</param>
    /// <param name="intervalMs">The monitored-item interval, in milliseconds, for the subscription.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000)
    {
        if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return;

        // Skip if already subscribed
        if (ActiveSubscriptions.Any(s => s.NodeId == nodeIdStr)) return;

        try
        {
            var nodeId = NodeId.Parse(nodeIdStr);
            await _service.SubscribeAsync(nodeId, intervalMs);

            _dispatcher.Post(() =>
            {
                ActiveSubscriptions.Add(new SubscriptionItemViewModel(nodeIdStr, intervalMs));
                SubscriptionCount = ActiveSubscriptions.Count;
                StatusMessage = null;
            });
        }
        catch (Exception ex)
        {
            Logger.Warning(ex, "AddSubscriptionForNode failed for {NodeId}", nodeIdStr);
            _dispatcher.Post(() => StatusMessage = $"Subscribe failed for {nodeIdStr}: {ex.Message}");
        }
    }

    /// <summary>
    ///     Subscribes to a node and all its Variable descendants recursively.
    ///     Object nodes are browsed for children; Variable nodes are subscribed directly.
    /// </summary>
    /// <param name="nodeIdStr">The root node whose variables should be subscribed recursively.</param>
    /// <param name="nodeClass">The node class of the starting node so variables can be subscribed immediately.</param>
    /// <param name="intervalMs">The monitored-item interval, in milliseconds, used for created subscriptions.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs = 1000)
    {
        return AddSubscriptionRecursiveAsync(nodeIdStr, nodeClass, intervalMs, maxDepth: 10, currentDepth: 0);
    }

    private async Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs, int maxDepth, int currentDepth)
    {
        if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return;
        if (currentDepth >= maxDepth) return;

        if (nodeClass == "Variable")
        {
            await AddSubscriptionForNodeAsync(nodeIdStr, intervalMs);
            return;
        }

        // Browse children and recurse
        try
        {
            var nodeId = NodeId.Parse(nodeIdStr);
            var children = await _service.BrowseAsync(nodeId);

            foreach (var child in children)
                await AddSubscriptionRecursiveAsync(child.NodeId, child.NodeClass, intervalMs, maxDepth, currentDepth + 1);
        }
        catch (Exception ex)
        {
            Logger.Warning(ex, "Recursive browse failed for {NodeId}; skipping subtree", nodeIdStr);
            _dispatcher.Post(() => StatusMessage = $"Browse failed for {nodeIdStr}: {ex.Message}");
        }
    }

    /// <summary>
    ///     Returns the node IDs of all active subscriptions for persistence.
    /// </summary>
    /// <returns>The node IDs of all currently active subscriptions.</returns>
    public List<string> GetSubscribedNodeIds()
    {
        return ActiveSubscriptions.Select(s => s.NodeId).ToList();
    }

    /// <summary>
    ///     Restores subscriptions from a saved list of node IDs.
    /// </summary>
    /// <param name="nodeIds">The node IDs persisted from a prior UI session.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task RestoreSubscriptionsAsync(IEnumerable<string> nodeIds)
    {
        foreach (var nodeId in nodeIds)
            await AddSubscriptionForNodeAsync(nodeId);
    }

    /// <summary>
    ///     Reads the current value of a node to determine its type, validates that the raw
    ///     input can be parsed to that type, writes the value, and returns (success, message).
    /// </summary>
    /// <param name="nodeIdStr">The node ID the operator wants to write.</param>
    /// <param name="rawValue">The raw text value entered by the operator.</param>
    /// <returns>A tuple indicating whether the write succeeded and a human-readable status message.</returns>
    public async Task<(bool Success, string Message)> ValidateAndWriteAsync(string nodeIdStr, string rawValue)
    {
        try
        {
            var nodeId = NodeId.Parse(nodeIdStr);

            // Read current value to determine target type
            var currentDataValue = await _service.ReadValueAsync(nodeId);
            var currentValue = currentDataValue.Value;

            // Try parsing to the target type before writing
            try
            {
                Shared.Helpers.ValueConverter.ConvertValue(rawValue, currentValue);
            }
            catch (FormatException ex)
            {
                var typeName = currentValue?.GetType().Name ?? "unknown";
                return (false, $"Cannot parse \"{rawValue}\" as {typeName}: {ex.Message}");
            }
            catch (OverflowException ex)
            {
                var typeName = currentValue?.GetType().Name ?? "unknown";
                return (false, $"Value \"{rawValue}\" is out of range for {typeName}: {ex.Message}");
            }

            var result = await _service.WriteValueAsync(nodeId, rawValue);
            var statusText = Helpers.StatusCodeFormatter.Format(result);

            if (Opc.Ua.StatusCode.IsGood(result))
                return (true, statusText);

            return (false, $"Write failed: {statusText}");
        }
        catch (Exception ex)
        {
            return (false, $"Error: {ex.Message}");
        }
    }

    /// <summary>
    ///     Clears all subscriptions and resets state.
    /// </summary>
    public void Clear()
    {
        ActiveSubscriptions.Clear();
        SubscriptionCount = 0;
    }

    /// <summary>
    ///     Re-hooks event handlers to the service after a server-side reconnect.
    ///     Safe to call when already attached (duplicate += is a no-op in .NET multicast delegates).
    /// </summary>
    public void Reattach()
    {
        _service.DataChanged -= OnDataChanged;
        _service.DataChanged += OnDataChanged;
    }

    /// <summary>
    ///     Unhooks event handlers from the service.
    /// </summary>
    public void Teardown()
    {
        _service.DataChanged -= OnDataChanged;
    }
}
