using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace MxAccess.Cli.Mx
{
    /// JSONL parser shared by the batch commands. Lines are read lazily so
    /// huge inputs don't allocate up front; per-line parsing is delegated to
    /// `ParseReadEntry` / `ParseWriteEntry` so the read loop can defer parse
    /// errors to a context where it can attribute them to a line number.
    ///
    /// Line conventions (same across read / write / subscribe):
    ///   - Blank lines and lines starting with `#` are skipped (line numbers
    ///     still count them so error messages stay accurate against the file).
    ///   - Path `-` reads from stdin until EOF.
    public static class JsonlInputReader
    {
        public static IEnumerable<(int LineNumber, string Json)> ReadLines(string pathOrDash)
        {
            if (string.IsNullOrEmpty(pathOrDash))
                throw new InvalidBatchInputException("Input path must be non-empty (use '-' for stdin).");

            TextReader reader = pathOrDash == "-"
                ? Console.In
                : OpenFile(pathOrDash);

            try
            {
                int lineNo = 0;
                string raw;
                while ((raw = reader.ReadLine()) != null)
                {
                    lineNo++;
                    var trimmed = raw.Trim();
                    if (trimmed.Length == 0) continue;
                    if (trimmed[0] == '#')   continue;
                    yield return (lineNo, trimmed);
                }
            }
            finally
            {
                // Don't close Console.In — the host CLR owns it and closing it
                // breaks anything that reads stdin after this command returns.
                if (pathOrDash != "-") reader.Dispose();
            }
        }

        private static TextReader OpenFile(string path)
        {
            try
            {
                return new StreamReader(path);
            }
            catch (FileNotFoundException)
            {
                throw new InvalidBatchInputException($"Input file not found: {path}");
            }
            catch (DirectoryNotFoundException)
            {
                throw new InvalidBatchInputException($"Input file not found: {path}");
            }
            catch (IOException ex)
            {
                throw new InvalidBatchInputException($"Could not open input file '{path}': {ex.Message}");
            }
            catch (UnauthorizedAccessException ex)
            {
                throw new InvalidBatchInputException($"Could not open input file '{path}': {ex.Message}");
            }
        }

        /// Parse a read / subscribe entry. Accepts either a bare JSON string
        /// (the tag reference) or a `{"tag":"..."}` object. Throws
        /// `InvalidBatchEntryException` on malformed input — the caller is
        /// expected to surface line number + reason in the result envelope.
        public static BatchReadEntry ParseReadEntry(int lineNumber, string json)
        {
            JToken token;
            try
            {
                token = JToken.Parse(json);
            }
            catch (JsonReaderException ex)
            {
                throw new InvalidBatchEntryException(lineNumber, $"invalid JSON: {ex.Message}");
            }

            string tag;
            switch (token.Type)
            {
                case JTokenType.String:
                    tag = (string)token;
                    break;
                case JTokenType.Object:
                    var tagTok = token["tag"];
                    if (tagTok == null || tagTok.Type != JTokenType.String)
                        throw new InvalidBatchEntryException(lineNumber,
                            "object entry must have a 'tag' string field");
                    tag = (string)tagTok;
                    break;
                default:
                    throw new InvalidBatchEntryException(lineNumber,
                        "expected a tag string or an object with a 'tag' field");
            }

            if (string.IsNullOrWhiteSpace(tag))
                throw new InvalidBatchEntryException(lineNumber, "tag must be non-empty");

            return new BatchReadEntry { Tag = tag, LineNumber = lineNumber };
        }

        /// Parse a write entry. Required: `tag` (string), `value` (any JSON
        /// type). Optional: `type` (string, mirrors `mxa write --type`).
        /// If `value` is a JSON array the tag must end in `[]` (matches the
        /// array-write rule in `WriteCommand`).
        public static BatchWriteEntry ParseWriteEntry(int lineNumber, string json)
        {
            JToken token;
            try
            {
                token = JToken.Parse(json);
            }
            catch (JsonReaderException ex)
            {
                throw new InvalidBatchEntryException(lineNumber, $"invalid JSON: {ex.Message}");
            }

            if (token.Type != JTokenType.Object)
                throw new InvalidBatchEntryException(lineNumber,
                    "write entry must be a JSON object with at least 'tag' and 'value'");

            var tagTok   = token["tag"];
            var valueTok = token["value"];
            var typeTok  = token["type"];

            if (tagTok == null || tagTok.Type != JTokenType.String)
                throw new InvalidBatchEntryException(lineNumber, "'tag' must be a non-empty string");
            var tag = (string)tagTok;
            if (string.IsNullOrWhiteSpace(tag))
                throw new InvalidBatchEntryException(lineNumber, "'tag' must be a non-empty string");

            if (valueTok == null)
                throw new InvalidBatchEntryException(lineNumber, "'value' is required");

            string typeHint = null;
            if (typeTok != null)
            {
                if (typeTok.Type != JTokenType.String)
                    throw new InvalidBatchEntryException(lineNumber, "'type' must be a string when present");
                typeHint = (string)typeTok;
            }

            bool tagIsArray = tag.EndsWith("[]", StringComparison.Ordinal);
            bool valueIsArray = valueTok.Type == JTokenType.Array;

            if (valueIsArray && !tagIsArray)
                throw new InvalidBatchEntryException(lineNumber,
                    "array 'value' requires a tag ending in '[]' (whole-array write reference)");
            if (!valueIsArray && tagIsArray)
                throw new InvalidBatchEntryException(lineNumber,
                    "tag ends in '[]' (array write) but 'value' is not a JSON array");

            return new BatchWriteEntry
            {
                Tag        = tag,
                RawValue   = valueTok,
                TypeHint   = typeHint,
                IsArray    = tagIsArray,
                LineNumber = lineNumber,
            };
        }
    }

    public sealed class BatchReadEntry
    {
        public string Tag { get; init; }
        public int LineNumber { get; init; }
    }

    public sealed class BatchWriteEntry
    {
        public string Tag { get; init; }
        public JToken RawValue { get; init; }
        public string TypeHint { get; init; }
        public bool IsArray { get; init; }
        public int LineNumber { get; init; }
    }

    /// Thrown for problems with the file itself (missing, unreadable, etc).
    /// Distinct from `InvalidBatchEntryException` so the command can choose
    /// to abort the whole batch on this category without trying to parse.
    public sealed class InvalidBatchInputException : Exception
    {
        public InvalidBatchInputException(string message) : base(message) { }
    }

    /// Thrown for problems with a single line. Always carries the 1-based line
    /// number so the command can echo it back in the envelope.
    public sealed class InvalidBatchEntryException : Exception
    {
        public int LineNumber { get; }
        public InvalidBatchEntryException(int lineNumber, string reason) : base(reason)
        {
            LineNumber = lineNumber;
        }
    }
}
