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("write-batch", Description =
        "Write many tag/value pairs from a JSONL input file (or stdin via '-'). " +
        "Pipelines per-item resolve and write across one MxSession so wall time is " +
        "~max(resolve) + ~max(write_ack) instead of N × (resolve + write_ack).")]
    public sealed class WriteBatchCommand : ICommand
    {
        [CommandParameter(0, Name = "input",
            Description = "Path to a JSONL file. Use '-' to read JSONL from stdin. " +
                          "Each line: {\"tag\":\"...\", \"value\":..., \"type\":\"int\"}.")]
        public string Input { get; init; }

        [CommandOption("timeout", 't', Description = "Per-phase timeout in seconds (one wall budget for resolve, one for write_ack). Default 5.")]
        public double TimeoutSeconds { get; init; } = 5.0;

        [CommandOption("user-id", Description = "Pre-resolved authenticated user id (0 = unauthenticated). See `mxa write` for how to obtain.")]
        public int UserId { get; init; }

        [CommandOption("username", 'u', Description = "Galaxy / OS username. Combined with --domain as '<domain>\\<username>' before AuthenticateUser.")]
        public string Username { get; init; }

        [CommandOption("domain", Description = "Domain or hostname for OS-authenticated galaxies.")]
        public string Domain { get; init; }

        [CommandOption("password", 'p', Description = "Password for --username. Redacted in the JSON query echo.")]
        public string Password { get; init; }

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

        [CommandOption("secured", Description = "Route writes through WriteSecured(currentUserId, verifierUserId, value) instead of Write(value, userId). Required for Secured Write / Verified Write classifications.")]
        public bool Secured { get; init; }

        [CommandOption("verifier-username", Description = "Galaxy / OS username of the verifier for a two-person Verified Write. Implies --secured.")]
        public string VerifierUsername { get; init; }

        [CommandOption("verifier-domain", Description = "Domain or hostname for the verifier OS user.")]
        public string VerifierDomain { get; init; }

        [CommandOption("verifier-password", Description = "Password for the verifier user. Redacted in the JSON query echo.")]
        public string VerifierPassword { get; init; }

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

        // Per-entry state carried across Phase A (resolve) and Phase B (write).
        private sealed class EntryState
        {
            public BatchWriteEntry Entry;
            public MxItem Item;             // null if AddItem failed
            public string AddItemError;
            public MxUpdate ResolveUpdate;  // null if no DataChange arrived
            public string ResolveError;     // "timeout-resolving-type" | "type-resolution-failed" | null
            public object CoercedValue;
            public string CoerceError;      // raised by ValueCoercion.CoerceJToken
            public MxUpdate AckUpdate;      // null if no WriteComplete arrived
            public string AckError;         // "timeout" | "write-failed" | null
        }

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

            // Compose the verify-user strings up front so the query echo
            // reflects exactly what we'll send to AuthenticateUser.
            string verifyUser = null;
            if (!string.IsNullOrEmpty(Username))
                verifyUser = string.IsNullOrEmpty(Domain) ? Username : $@"{Domain}\{Username}";
            string verifierVerifyUser = null;
            if (!string.IsNullOrEmpty(VerifierUsername))
                verifierVerifyUser = string.IsNullOrEmpty(VerifierDomain)
                    ? VerifierUsername
                    : $@"{VerifierDomain}\{VerifierUsername}";
            bool useSecured = Secured || verifierVerifyUser != null;

            // Parse + validate the whole input before opening an LMX session.
            // Aborting on parse errors avoids registering a session we'll
            // immediately tear down, and gives the user every malformed line
            // in one report.
            var entries = new List<BatchWriteEntry>();
            var parseErrors = new List<object>();
            try
            {
                foreach (var (lineNo, json) in JsonlInputReader.ReadLines(Input))
                {
                    try
                    {
                        entries.Add(JsonlInputReader.ParseWriteEntry(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)
            {
                EmitEnvelopeOrError(console, BuildQuery(verifyUser, verifierVerifyUser, useSecured, entryCount: 0),
                                    ok: false, results: null, topError: ex.Message);
                Environment.ExitCode = 2;
                return default;
            }

            var query = BuildQuery(verifyUser, verifierVerifyUser, useSecured, entries.Count);

            if (parseErrors.Count > 0)
            {
                EmitEnvelopeOrError(console, query, ok: false, parseErrors, topError: null);
                Environment.ExitCode = 2;
                return default;
            }

            if (entries.Count == 0)
            {
                EmitEnvelopeOrError(console, query, ok: false, results: null, topError: "empty-input");
                Environment.ExitCode = 2;
                return default;
            }

            using var session = new MxSession(ClientName);

            // Auth resolution. Anything other than userId>0 here is a hard
            // abort: writes downstream depend on a valid identity, and an
            // empty results[] is the right shape for "no items attempted".
            int effectiveUserId = UserId;
            int verifierUserId  = 0;
            if (verifyUser != null)
            {
                effectiveUserId = session.Authenticate(verifyUser, Password ?? string.Empty);
                if (effectiveUserId == 0)
                {
                    EmitEnvelopeOrError(console, query, ok: false, results: null, topError: "authentication-failed");
                    Environment.ExitCode = 1;
                    return default;
                }
            }
            if (verifierVerifyUser != null)
            {
                verifierUserId = session.Authenticate(verifierVerifyUser, VerifierPassword ?? string.Empty);
                if (verifierUserId == 0)
                {
                    EmitEnvelopeOrError(console, query, ok: false, results: null, topError: "verifier-authentication-failed");
                    Environment.ExitCode = 1;
                    return default;
                }
            }
            else if (useSecured)
            {
                verifierUserId = effectiveUserId;
            }

            var states = new EntryState[entries.Count];
            for (int i = 0; i < entries.Count; i++) states[i] = new EntryState { Entry = entries[i] };

            try
            {
                // ─── Phase A: AddItem + Advise for everything, then drain ───────────
                //
                // Per WriteCommand.cs:178-179: --username uses Advise (operator
                // action); anonymous uses AdviseSupervisory (supervisory action).
                // Same heuristic here so the audit trail attribution matches
                // the single-tag write.
                bool useOperatorAdvise = verifyUser != null;
                var pendingResolve = new HashSet<int>();
                foreach (var s in states)
                {
                    try
                    {
                        s.Item = session.AddItem(s.Entry.Tag);
                        if (useOperatorAdvise) s.Item.Advise();
                        else                   s.Item.AdviseSupervisory();
                        pendingResolve.Add(s.Item.Handle);
                    }
                    catch (Exception ex)
                    {
                        s.AddItemError = ex.Message;
                    }
                }

                var phaseADeadline = DateTime.UtcNow.AddSeconds(TimeoutSeconds);
                var resolveByHandle = new Dictionary<int, MxUpdate>();
                while (pendingResolve.Count > 0)
                {
                    var remaining = phaseADeadline - DateTime.UtcNow;
                    if (remaining <= TimeSpan.Zero) break;
                    if (!session.WaitForUpdate(
                            u => u.Kind == MxUpdateKind.DataChange && pendingResolve.Contains(u.ItemHandle),
                            remaining, out var u))
                        break;
                    resolveByHandle[u.ItemHandle] = u;
                    pendingResolve.Remove(u.ItemHandle);
                }

                // Annotate each state with its resolve outcome.
                foreach (var s in states)
                {
                    if (s.Item == null) continue;
                    if (resolveByHandle.TryGetValue(s.Item.Handle, out var u))
                    {
                        s.ResolveUpdate = u;
                        if (!u.IsOk) s.ResolveError = "type-resolution-failed";
                    }
                    else
                    {
                        s.ResolveError = "timeout-resolving-type";
                    }
                }

                // ─── Phase B: coerce values, fire writes, drain WriteComplete ───────
                //
                // Coerce up front so a bad value doesn't queue a Write the proxy
                // would reject anyway. Track which handles we actually issued
                // writes for — only those join the pendingAck set.
                var pendingAck = new HashSet<int>();
                foreach (var s in states)
                {
                    if (s.Item == null || s.ResolveError != null) continue;
                    try
                    {
                        s.CoercedValue = ValueCoercion.CoerceJToken(s.Entry.RawValue, s.Entry.TypeHint, s.Entry.IsArray);
                    }
                    catch (Exception ex)
                    {
                        s.CoerceError = ex.Message;
                        continue;
                    }
                    try
                    {
                        if (useSecured) s.Item.WriteSecured(s.CoercedValue, effectiveUserId, verifierUserId);
                        else            s.Item.Write(s.CoercedValue, effectiveUserId);
                        pendingAck.Add(s.Item.Handle);
                    }
                    catch (Exception ex)
                    {
                        s.AckError = "write-call-failed: " + ex.Message;
                    }
                }

                var phaseBDeadline = DateTime.UtcNow.AddSeconds(TimeoutSeconds);
                var ackByHandle = new Dictionary<int, MxUpdate>();
                while (pendingAck.Count > 0)
                {
                    var remaining = phaseBDeadline - DateTime.UtcNow;
                    if (remaining <= TimeSpan.Zero) break;
                    if (!session.WaitForUpdate(
                            u => u.Kind == MxUpdateKind.WriteComplete && pendingAck.Contains(u.ItemHandle),
                            remaining, out var u))
                        break;
                    ackByHandle[u.ItemHandle] = u;
                    pendingAck.Remove(u.ItemHandle);
                }

                foreach (var s in states)
                {
                    if (s.Item == null || s.ResolveError != null || s.CoerceError != null || s.AckError != null) continue;
                    if (ackByHandle.TryGetValue(s.Item.Handle, out var u))
                    {
                        s.AckUpdate = u;
                        if (!u.IsOk) s.AckError = "write-failed";
                    }
                    else
                    {
                        s.AckError = "timeout";
                    }
                }

                // ─── Build results in input order ───────────────────────────────────
                var results = new List<object>(states.Length);
                bool anyFailed = false;
                foreach (var s in states)
                {
                    var (ok, error, statuses) = Classify(s);
                    if (!ok) anyFailed = true;
                    results.Add(new
                    {
                        tag              = s.Entry.Tag,
                        line             = s.Entry.LineNumber,
                        ok,
                        error,
                        authenticated    = verifyUser != null,
                        auth_user_id     = verifyUser != null ? (int?)effectiveUserId : null,
                        secured          = useSecured,
                        verifier_user_id = useSecured ? (int?)verifierUserId : null,
                        statuses,
                    });
                }

                if (LlmJson) Envelope.Write(console, query, ok: !anyFailed, results);
                else         WriteHuman(console, results);
                if (anyFailed) Environment.ExitCode = 1;
            }
            finally
            {
                foreach (var s in states)
                    if (s.Item != null) try { s.Item.Dispose(); } catch { }
            }
            return default;
        }

        private static (bool ok, string error, MxStatusInfo[] statuses) Classify(EntryState s)
        {
            if (s.AddItemError != null)
                return (false, "add-item-failed: " + s.AddItemError, Array.Empty<MxStatusInfo>());
            if (s.ResolveError != null)
                return (false, s.ResolveError, s.ResolveUpdate?.Statuses ?? Array.Empty<MxStatusInfo>());
            if (s.CoerceError != null)
                return (false, "value-coerce-failed: " + s.CoerceError, Array.Empty<MxStatusInfo>());
            if (s.AckError != null)
                return (false, s.AckError, s.AckUpdate?.Statuses ?? Array.Empty<MxStatusInfo>());
            return (true, null, s.AckUpdate?.Statuses ?? Array.Empty<MxStatusInfo>());
        }

        private object BuildQuery(string verifyUser, string verifierVerifyUser, bool useSecured, int entryCount) =>
            new
            {
                command              = "write-batch",
                input                = Input,
                entries              = entryCount,
                timeout_s            = TimeoutSeconds,
                user_id              = UserId,
                verify_user          = verifyUser,
                verifier_verify_user = verifierVerifyUser,
                secured              = useSecured,
                password             = string.IsNullOrEmpty(Password)         ? null : "***",
                verifier_password    = string.IsNullOrEmpty(VerifierPassword) ? null : "***",
                client               = ClientName,
            };

        // Single emit-point used for parse / auth / empty-input failures so
        // the envelope shape stays consistent whether the failure is a top-
        // level error or a results-array of per-line errors.
        private void EmitEnvelopeOrError(IConsole console, object query, bool ok,
                                         IEnumerable<object> results, string topError)
        {
            if (LlmJson)
            {
                if (results != null)
                    Envelope.Write(console, query, ok, results);
                else
                    Envelope.WriteError(console, query, topError);
            }
            else if (topError != null)
            {
                console.Error.WriteLine($"[ERR] {topError}");
            }
            else if (results != null)
            {
                foreach (dynamic r in results)
                {
                    var line = r.line;
                    var reason = (string)(r.reason ?? r.error);
                    console.Error.WriteLine($"[ERR] line {line}: {reason}");
                }
            }
        }

        private static void WriteHuman(IConsole console, List<object> results)
        {
            foreach (dynamic r in results)
            {
                if ((bool)r.ok)
                    console.Output.WriteLine($"[OK ] line {r.line} write {r.tag}");
                else
                    console.Error.WriteLine($"[ERR] line {r.line} write {r.tag}: {r.error}");
            }
        }
    }
}
