using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace PCL.Core.App.Cli;

/// <summary>
/// 命令行模型
/// </summary>
[JsonConverter(typeof(CommandLineJsonConverter))]
public class CommandLine
{
    /// <summary>
    /// 命令文本
    /// </summary>
    public required string CommandText { get; init; }

    /// <summary>
    /// 子命令
    /// </summary>
    public CommandLine? Subcommand { get; init; } = null;

    /// <summary>
    /// 子命令文本
    /// </summary>
    public string? SubcommandText => Subcommand?.CommandText;

    /// <summary>
    /// 参数字典
    /// </summary>
    public required IReadOnlyDictionary<string, CommandArgument> Arguments { get; init; }

    /// <summary>
    /// 尝试获取参数值
    /// </summary>
    /// <param name="key">参数键</param>
    /// <param name="value">参数值，若获取失败则为对应类型默认值</param>
    /// <typeparam name="TValue">参数值的类型</typeparam>
    /// <returns>是否存在该键; 存在该键时值的类型是否匹配</returns>
    public (bool exists, bool isTypeMatch) TryGetArgumentValue<TValue>(string key, out TValue? value)
    {
        var exists = Arguments.TryGetValue(key, out var arg);
        var isTypeMatch = false;
        if (exists && (isTypeMatch = arg!.TryCastValue(out TValue? typedValue)))
        {
            value = typedValue;
            return (true, true);
        }
        value = default;
        return (exists, isTypeMatch);
    }

    /// <summary>
    /// 解析参数数组，第一个元素会被视为主命令
    /// </summary>
    /// <param name="args">参数数组</param>
    /// <param name="subcommands">各级子命令列表</param>
    /// <returns>命令行模型实例</returns>
    public static CommandLine Parse(ReadOnlySpan<string> args, IEnumerable<SubcommandDefinition>? subcommands = null)
    {
        subcommands ??= [];
        SubcommandDefinition root = (args[0], subcommands);
        return CommandLineParser.Parse(args, root);
    }

    public override string ToString()
    {
        var sb = new StringBuilder();
        sb.Append(CommandText).Append(" [");
        if (Arguments.Count > 0) sb.AppendLine();
        foreach (var arg in Arguments.Values)
        {
            sb.Append("  --").Append(arg.Key);
            var value = arg.ValueKind switch
            {
                ArgumentValueKind.Bool => arg.CastValue<bool>() ? "true" : "false",
                ArgumentValueKind.Decimal => arg.CastValue<decimal>().ToString(CultureInfo.InvariantCulture),
                ArgumentValueKind.Text => arg.ValueText,
                _ => null
            };
            if (value is not null) sb.Append(": ").Append(value);
            sb.AppendLine();
        }
        sb.Append(']');
        if (Subcommand is not null) sb.AppendLine().Append("-> ").Append(Subcommand.ToString().Replace("\n", "\n   "));
        return sb.ToString();
    }
}

file static class CommandLineParser
{
    private static (CommandArgument, bool) _ParseArgument(string key, string possibleValueText)
    {
        if (key.StartsWith("--")) key = key[2..];
        if (possibleValueText.Length == 0 || possibleValueText.StartsWith("--"))
            return (new BoolArgument { Key = key, ValueText = string.Empty }, false);
        if (possibleValueText.ToLowerInvariant() is "true" or "false")
            return (new BoolArgument { Key = key, ValueText = possibleValueText }, true);
        if (decimal.TryParse(possibleValueText, out var d))
            return (new DecimalArgument { Key = key, ValueText = possibleValueText, Value = d }, true);
        return (new TextArgument { Key = key, ValueText = possibleValueText }, true);
    }

    public static CommandLine Parse(ReadOnlySpan<string> args, SubcommandDefinition subcommands)
    {
        if (args.IsEmpty) throw new ArgumentException("The argument span must contain at least 1 element", nameof(args));
        var i = 1;
        var commandText = args[0];
        var argumentList = new Dictionary<string, CommandArgument>();
        CommandLine? subcommand = null;
        while (i < args.Length)
        {
            var currentText = args[i];
            if (subcommands.Contains(currentText))
            {
                subcommand = Parse(args[i..], subcommands.SubcommandMap[currentText]);
                break;
            }
            var (commandArgument, hasValueText) = _ParseArgument(currentText,
                (i == args.Length - 1) || (subcommands.Contains(args[i + 1])) ? "" : args[i + 1]);
            argumentList[commandArgument.Key] = commandArgument;
            i += hasValueText ? 2 : 1;
        }
        return new CommandLine
        {
            CommandText = commandText,
            Arguments = argumentList.AsReadOnly(),
            Subcommand = subcommand
        };
    }
}

/// <summary>
/// 用于 <see cref="CommandLine"/> 的 JSON 转换器
/// </summary>
// Generated by gpt-5.3-codex (20260218)
public sealed class CommandLineJsonConverter : JsonConverter<CommandLine>
{
    public override CommandLine? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null) return null;
        if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected object for CommandLine.");

        string? commandText = null;
        CommandLine? subcommand = null;
        var arguments = new Dictionary<string, CommandArgument>();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject) break;
            if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected property name.");

            var propName = reader.GetString();
            if (!reader.Read()) throw new JsonException("Unexpected end of json.");

            switch (propName)
            {
                case "cmd":
                    commandText = reader.GetString() ?? throw new JsonException("cmd cannot be null.");
                    break;
                case "sub":
                    subcommand = JsonSerializer.Deserialize<CommandLine>(ref reader, options);
                    break;
                case "args":
                    _ReadArguments(ref reader, arguments);
                    break;
                default:
                    // 忽略未知字段，提升前向兼容
                    reader.Skip();
                    break;
            }
        }

        if (string.IsNullOrEmpty(commandText)) throw new JsonException("Missing required property: cmd.");

        return new CommandLine
        {
            CommandText = commandText,
            Arguments = arguments.AsReadOnly(),
            Subcommand = subcommand
        };
    }

    public override void Write(Utf8JsonWriter writer, CommandLine value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        writer.WriteString("cmd", value.CommandText);
        if (value.Arguments.Count > 0)
        {
            writer.WritePropertyName("args");
            writer.WriteStartArray();
            foreach (var arg in value.Arguments.Values)
            {
                writer.WriteStartObject();
                writer.WriteString("k", arg.Key);
                writer.WriteNumber("t", (int)arg.ValueKind);
                writer.WriteString("v", arg.ValueText);
                writer.WriteEndObject();
            }
            writer.WriteEndArray();
        }
        if (value.Subcommand is not null)
        {
            writer.WritePropertyName("sub");
            JsonSerializer.Serialize(writer, value.Subcommand, options);
        }
        writer.WriteEndObject();
    }

    private static void _ReadArguments(ref Utf8JsonReader reader, Dictionary<string, CommandArgument> target)
    {
        if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("args must be an array.");

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray) return;
            if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Argument item must be an object.");

            string? key = null;
            ArgumentValueKind? kind = null;
            string? valueText = null;

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject) break;
                if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected argument property name.");

                var name = reader.GetString();
                if (!reader.Read()) throw new JsonException("Unexpected end of json.");

                switch (name)
                {
                    case "k":
                        key = reader.GetString();
                        break;
                    case "t":
                        kind = (ArgumentValueKind)reader.GetInt32();
                        break;
                    case "v":
                        valueText = reader.GetString() ?? string.Empty;
                        break;
                    default:
                        reader.Skip();
                        break;
                }
            }

            if (string.IsNullOrEmpty(key)) throw new JsonException("Argument missing key(k).");
            if (kind is null) throw new JsonException("Argument missing type(t).");
            valueText ??= string.Empty;

            CommandArgument arg = kind.Value switch
            {
                ArgumentValueKind.Bool => new BoolArgument { Key = key, ValueText = valueText },
                ArgumentValueKind.Decimal => new DecimalArgument { Key = key, ValueText = valueText },
                ArgumentValueKind.Text => new TextArgument { Key = key, ValueText = valueText },
                _ => throw new JsonException($"Unsupported argument type: {(int)kind.Value}")
            };
            target[arg.Key] = arg;
        }
        throw new JsonException("args array is not closed.");
    }
}
