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

namespace MxAccess.Cli.Mx
{
    /// MxAccess's Write() takes an object and the LMX proxy figures out the
    /// type by looking at the destination attribute. The CLI accepts strings
    /// and either trusts the proxy (default) or coerces to a caller-specified
    /// .NET type up-front so the LMX side gets exactly what we mean.
    public static class ValueCoercion
    {
        public static object Coerce(string raw, string typeHint)
        {
            if (raw == null) throw new ArgumentNullException(nameof(raw));

            if (string.IsNullOrEmpty(typeHint))
                return InferAndCoerce(raw);

            switch (typeHint.Trim().ToLowerInvariant())
            {
                case "bool":   return ParseBool(raw);
                case "byte":   return byte.Parse(raw, CultureInfo.InvariantCulture);
                case "short":  return short.Parse(raw, CultureInfo.InvariantCulture);
                case "int":
                case "int32":  return int.Parse(raw, CultureInfo.InvariantCulture);
                case "long":
                case "int64":  return long.Parse(raw, CultureInfo.InvariantCulture);
                case "float":
                case "single": return float.Parse(raw, CultureInfo.InvariantCulture);
                case "double": return double.Parse(raw, CultureInfo.InvariantCulture);
                case "string": return raw;
                case "time":
                case "datetime":
                    return DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal);
                default:
                    throw new ArgumentException(
                        $"Unknown --type '{typeHint}'. Supported: bool, byte, short, int, long, float, double, string, datetime.");
            }
        }

        /// Build a strongly-typed array (`bool[]`, `int[]`, `string[]`, …) from
        /// a list of raw string values. The CLR marshals strongly-typed arrays
        /// to a COM SAFEARRAY of the matching VARTYPE, which is what MxAccess
        /// expects for an array attribute write.
        public static object CoerceArray(IReadOnlyList<string> values, string typeHint)
        {
            if (values == null) throw new ArgumentNullException(nameof(values));

            // Default array element type when --type isn't specified: peek at
            // the first value's inferred type and assume the rest match. For
            // an empty list, fall back to string[].
            string effectiveHint = typeHint;
            if (string.IsNullOrEmpty(effectiveHint))
            {
                if (values.Count == 0) effectiveHint = "string";
                else
                {
                    var first = InferAndCoerce(values[0]);
                    effectiveHint = first switch
                    {
                        bool   _ => "bool",
                        int    _ => "int",
                        long   _ => "long",
                        double _ => "double",
                        _        => "string",
                    };
                }
            }

            switch (effectiveHint.Trim().ToLowerInvariant())
            {
                case "bool":   return Convert<bool>(values, s => ParseBool(s));
                case "byte":   return Convert<byte>(values, s => byte.Parse(s, CultureInfo.InvariantCulture));
                case "short":  return Convert<short>(values, s => short.Parse(s, CultureInfo.InvariantCulture));
                case "int":
                case "int32":  return Convert<int>(values, s => int.Parse(s, CultureInfo.InvariantCulture));
                case "long":
                case "int64":  return Convert<long>(values, s => long.Parse(s, CultureInfo.InvariantCulture));
                case "float":
                case "single": return Convert<float>(values, s => float.Parse(s, CultureInfo.InvariantCulture));
                case "double": return Convert<double>(values, s => double.Parse(s, CultureInfo.InvariantCulture));
                case "string": return Convert<string>(values, s => s);
                case "time":
                case "datetime":
                    return Convert<DateTime>(values, s => DateTime.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal));
                default:
                    throw new ArgumentException(
                        $"Unknown --type '{effectiveHint}'. Supported: bool, byte, short, int, long, float, double, string, datetime.");
            }
        }

        /// JSON-token entry point used by `read-batch` / `write-batch`. Renders
        /// each token to the string form the existing `Coerce` / `CoerceArray`
        /// dispatch table accepts, then forwards — so the type vocabulary
        /// stays defined in one place.
        public static object CoerceJToken(JToken raw, string typeHint, bool isArray)
        {
            if (raw == null) throw new ArgumentNullException(nameof(raw));

            if (isArray)
            {
                if (raw.Type != JTokenType.Array)
                    throw new ArgumentException("isArray=true but JSON token is not an array.");
                var rendered = new List<string>();
                foreach (var el in (JArray)raw) rendered.Add(RenderToken(el));
                return CoerceArray(rendered, typeHint);
            }
            return Coerce(RenderToken(raw), typeHint);
        }

        private static string RenderToken(JToken t)
        {
            switch (t.Type)
            {
                case JTokenType.String:  return (string)t;
                case JTokenType.Boolean: return ((bool)t) ? "true" : "false";
                case JTokenType.Integer: return ((long)t).ToString(CultureInfo.InvariantCulture);
                case JTokenType.Float:   return ((double)t).ToString("R", CultureInfo.InvariantCulture);
                case JTokenType.Date:    return ((DateTime)t).ToString("o", CultureInfo.InvariantCulture);
                case JTokenType.Null:
                    throw new ArgumentException("JSON null is not a writable value.");
                default:
                    throw new ArgumentException($"Cannot coerce JSON token of type {t.Type} to a write value.");
            }
        }

        private static T[] Convert<T>(IReadOnlyList<string> raw, Func<string, T> parse)
        {
            var arr = new T[raw.Count];
            for (int i = 0; i < raw.Count; i++) arr[i] = parse(raw[i]);
            return arr;
        }

        private static object InferAndCoerce(string raw)
        {
            if (ParseBool(raw, out var b))           return b;
            if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i))   return i;
            if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l))  return l;
            if (double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var d))  return d;
            return raw;
        }

        private static bool ParseBool(string raw, out bool result)
        {
            switch (raw.Trim().ToLowerInvariant())
            {
                case "true": case "1": case "on": case "yes":  result = true;  return true;
                case "false": case "0": case "off": case "no": result = false; return true;
                default: result = false; return false;
            }
        }

        private static bool ParseBool(string raw)
        {
            if (ParseBool(raw, out var b)) return b;
            throw new ArgumentException($"Cannot parse '{raw}' as bool. Use true/false, 1/0, on/off, yes/no.");
        }
    }
}
