using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using MxAccess.Cli.Mx;
using MxAccess.Cli.Output;

namespace MxAccess.Cli.Commands
{
    [Command("read", Description = "Read one or more tag values. Subscribes briefly, captures the first OnDataChange per tag, then tears down.")]
    public sealed class ReadCommand : ICommand
    {
        [CommandParameter(0, Name = "tags", Description = "One or more tag references (e.g. 'TestMachine_001.Speed').")]
        public IReadOnlyList<string> Tags { get; init; }

        [CommandOption("timeout", 't', Description = "Per-tag timeout in seconds while waiting for the first value. Default 5.")]
        public double TimeoutSeconds { get; init; } = 5.0;

        [CommandOption("client", Description = "MxAccess client name passed to Register(). Default 'mxa'.")]
        public string ClientName { get; init; } = "mxa";

        [CommandOption("llm-json", Description = "Emit the JSON envelope { query, ok, results } instead of human-readable lines.")]
        public bool LlmJson { get; init; }

        public ValueTask ExecuteAsync(IConsole console)
        {
            if (Tags == null || Tags.Count == 0)
                throw new CommandException("At least one tag reference is required.", 2);
            if (TimeoutSeconds <= 0)
                throw new CommandException("--timeout must be positive.", 2);

            var query = new
            {
                command = "read",
                tags    = Tags.ToArray(),
                timeout_s = TimeoutSeconds,
                client    = ClientName,
            };

            var results = new List<object>();
            using var session = new MxSession(ClientName);

            // Add and advise everything up front so notifications can begin streaming
            // while we drain. AddItem is sync; Advise is async (item handle becomes
            // valid for the OnDataChange dispatch on the proxy callback path).
            var items = new List<MxItem>();
            try
            {
                foreach (var t in Tags)
                {
                    var item = session.AddItem(t);
                    item.Advise();
                    items.Add(item);
                }

                var pending = new HashSet<int>(items.Select(i => i.Handle));
                var captured = new Dictionary<int, MxUpdate>();
                var deadline = DateTime.UtcNow.AddSeconds(TimeoutSeconds);

                while (pending.Count > 0)
                {
                    var remaining = deadline - DateTime.UtcNow;
                    if (remaining <= TimeSpan.Zero) break;

                    if (!session.WaitForUpdate(
                            u => u.Kind == MxUpdateKind.DataChange && pending.Contains(u.ItemHandle),
                            remaining,
                            out var update))
                        break;

                    captured[update.ItemHandle] = update;
                    pending.Remove(update.ItemHandle);
                }

                foreach (var item in items)
                {
                    if (captured.TryGetValue(item.Handle, out var u))
                    {
                        results.Add(BuildResult(item.Reference, u, timedOut: false));
                    }
                    else
                    {
                        results.Add(BuildResult(item.Reference, null, timedOut: true));
                    }
                }

                bool overallOk = results.Cast<dynamic>().All(r => (bool)r.ok);

                if (LlmJson)
                    Envelope.Write(console, query, overallOk, results);
                else
                    WriteHuman(console, results);
            }
            finally
            {
                foreach (var item in items) item.Dispose();
            }
            return default;
        }

        private static object BuildResult(string reference, MxUpdate u, bool timedOut)
        {
            if (timedOut)
            {
                return new
                {
                    tag = reference,
                    ok = false,
                    error = "timeout",
                    value = (object)null,
                    quality = (int?)null,
                    timestamp = (DateTime?)null,
                    statuses = Array.Empty<MxStatusInfo>(),
                };
            }
            return new
            {
                tag       = reference,
                ok        = u.IsOk,
                value     = u.Value,
                quality   = u.Quality,
                timestamp = u.Timestamp,
                statuses  = u.Statuses,
            };
        }

        private static void WriteHuman(IConsole console, List<object> results)
        {
            foreach (dynamic r in results)
            {
                if (!(bool)r.ok)
                {
                    console.Output.WriteLine($"[ERR] {r.tag}: {(string)(r.error ?? "bad-status")}");
                    continue;
                }
                var ts = r.timestamp == null ? "" : ((DateTime)r.timestamp).ToString("yyyy-MM-dd HH:mm:ss.fff");
                console.Output.WriteLine($"[OK ] {r.tag} = {r.value} (q={r.quality}, t={ts})");
            }
        }
    }
}
