namespace Spectre.Console.Cli;

internal sealed class CommandExecutor
{
    private readonly ITypeRegistrar _registrar;

    public CommandExecutor(ITypeRegistrar registrar)
    {
        _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
        _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
    }

    public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
    {
        if (configuration == null)
        {
            throw new ArgumentNullException(nameof(configuration));
        }

        _registrar.RegisterInstance(typeof(IConfiguration), configuration);
        _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole());

        // Create the command model.
        var model = CommandModelBuilder.Build(configuration);
        _registrar.RegisterInstance(typeof(CommandModel), model);
        _registrar.RegisterDependencies(model);

        // No default command?
        if (model.DefaultCommand == null)
        {
            // Got at least one argument?
            var firstArgument = args.FirstOrDefault();
            if (firstArgument != null)
            {
                // Asking for version? Kind of a hack, but it's alright.
                // We should probably make this a bit better in the future.
                if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) ||
                    firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase))
                {
                    var console = configuration.Settings.Console.GetConsole();
                    console.WriteLine(ResolveApplicationVersion(configuration));
                    return 0;
                }
            }
        }

        // Parse and map the model against the arguments.
        var parser = new CommandTreeParser(model, configuration.Settings);
        var parsedResult = parser.Parse(args);
        _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);

        // Currently the root?
        if (parsedResult.Tree == null)
        {
            // Display help.
            configuration.Settings.Console.SafeRender(HelpWriter.Write(model));
            return 0;
        }

        // Get the command to execute.
        var leaf = parsedResult.Tree.GetLeafCommand();
        if (leaf.Command.IsBranch || leaf.ShowHelp)
        {
            // Branches can't be executed. Show help.
            configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command));
            return leaf.ShowHelp ? 0 : 1;
        }

        // Register the arguments with the container.
        _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining);

        // Create the resolver and the context.
        using (var resolver = new TypeResolverAdapter(_registrar.Build()))
        {
            var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data);

            // Execute the command tree.
            return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
        }
    }

    private static string ResolveApplicationVersion(IConfiguration configuration)
    {
        return
            configuration.Settings.ApplicationVersion ?? // potential override
            VersionHelper.GetVersion(Assembly.GetEntryAssembly());
    }

    private static Task<int> Execute(
        CommandTree leaf,
        CommandTree tree,
        CommandContext context,
        ITypeResolver resolver,
        IConfiguration configuration)
    {
        // Bind the command tree against the settings.
        var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver);
        configuration.Settings.Interceptor?.Intercept(context, settings);

        // Create and validate the command.
        var command = leaf.CreateCommand(resolver);
        var validationResult = command.Validate(context, settings);
        if (!validationResult.Successful)
        {
            throw CommandRuntimeException.ValidationFailed(validationResult);
        }

        // Execute the command.
        return command.Execute(context, settings);
    }
}