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-batch", Description =
        "Read many tags from a JSONL input file (or stdin via '-'). One MxSession " +
        "amortizes the LMX bind cost across the whole batch.")]
    public sealed class ReadBatchCommand : ICommand
    {
        [CommandParameter(0, Name = "input",
            Description = "Path to a JSONL file. Use '-' to read JSONL from stdin. " +
                          "Each line is either a bare tag string (e.g. \"TM.Speed\") or " +
                          "an object (e.g. {\"tag\":\"TM.Speed\"}). Blank and '#'-prefixed lines are skipped.")]
        public string Input { 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 (string.IsNullOrWhiteSpace(Input))
                throw new CommandException("Input path is required (use '-' for stdin).", 2);
            if (TimeoutSeconds <= 0)
                throw new CommandException("--timeout must be positive.", 2);

            var query = new
            {
                command   = "read-batch",
                input     = Input,
                timeout_s = TimeoutSeconds,
                client    = ClientName,
            };

            // Phase 1: load + parse. Collect all parse errors so the user gets
            // every malformed line in one report instead of fixing them one at
            // a time.
            var entries = new List<BatchReadEntry>();
            var parseErrors = new List<object>();
            try
            {
                foreach (var (lineNo, json) in JsonlInputReader.ReadLines(Input))
                {
                    try
                    {
                        entries.Add(JsonlInputReader.ParseReadEntry(lineNo, json));
                    }
                    catch (InvalidBatchEntryException ex)
                    {
                        parseErrors.Add(new
                        {
                            tag    = (string)null,
                            line   = ex.LineNumber,
                            ok     = false,
                            error  = "parse-error",
                            reason = ex.Message,
                        });
                    }
                }
            }
            catch (InvalidBatchInputException ex)
            {
                if (LlmJson) Envelope.WriteError(console, query, ex.Message);
                else         console.Error.WriteLine($"[ERR] {ex.Message}");
                Environment.ExitCode = 2;
                return default;
            }

            if (parseErrors.Count > 0)
            {
                if (LlmJson) Envelope.Write(console, query, ok: false, parseErrors);
                else
                {
                    foreach (dynamic e in parseErrors)
                        console.Error.WriteLine($"[ERR] line {e.line}: {e.reason}");
                }
                Environment.ExitCode = 2;
                return default;
            }

            if (entries.Count == 0)
            {
                if (LlmJson) Envelope.WriteError(console, query, "empty-input");
                else         console.Error.WriteLine("[ERR] no entries in input");
                Environment.ExitCode = 2;
                return default;
            }

            // Phase 2: AddItem + Advise for every entry inside one MxSession,
            // then drain DataChange events keyed by handle.
            using var session = new MxSession(ClientName);
            var items = new List<MxItem>();
            // Track each entry's handle (or a per-entry add-item error) so the
            // result row keeps its input ordering and line-number echo even
            // when AddItem fails mid-batch.
            var addError = new Dictionary<int, string>();   // entryIndex -> error
            var handleByEntry = new int[entries.Count];     // 0 = no item created
            try
            {
                for (int i = 0; i < entries.Count; i++)
                {
                    var entry = entries[i];
                    try
                    {
                        var item = session.AddItem(entry.Tag);
                        item.Advise();
                        items.Add(item);
                        handleByEntry[i] = item.Handle;
                    }
                    catch (Exception ex)
                    {
                        addError[i] = ex.Message;
                    }
                }

                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);
                }

                var results = new List<object>(entries.Count);
                for (int i = 0; i < entries.Count; i++)
                {
                    var entry = entries[i];
                    if (addError.TryGetValue(i, out var msg))
                    {
                        results.Add(new
                        {
                            tag       = entry.Tag,
                            line      = entry.LineNumber,
                            ok        = false,
                            error     = "add-item-failed",
                            reason    = msg,
                            value     = (object)null,
                            quality   = (int?)null,
                            timestamp = (DateTime?)null,
                            statuses  = Array.Empty<MxStatusInfo>(),
                        });
                        continue;
                    }
                    if (captured.TryGetValue(handleByEntry[i], out var u))
                    {
                        results.Add(new
                        {
                            tag       = entry.Tag,
                            line      = entry.LineNumber,
                            ok        = u.IsOk,
                            value     = u.Value,
                            quality   = u.Quality,
                            timestamp = u.Timestamp,
                            statuses  = u.Statuses,
                        });
                    }
                    else
                    {
                        results.Add(new
                        {
                            tag       = entry.Tag,
                            line      = entry.LineNumber,
                            ok        = false,
                            error     = "timeout",
                            value     = (object)null,
                            quality   = (int?)null,
                            timestamp = (DateTime?)null,
                            statuses  = Array.Empty<MxStatusInfo>(),
                        });
                    }
                }

                bool overallOk = results.Cast<dynamic>().All(r => (bool)r.ok);
                if (LlmJson) Envelope.Write(console, query, overallOk, results);
                else         WriteHuman(console, results);
                if (!overallOk) Environment.ExitCode = 1;
            }
            finally
            {
                foreach (var item in items) item.Dispose();
            }
            return default;
        }

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