using Docker.DotNet;
using Docker.DotNet.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace AnsonBot.Core;

public interface IRconService
{
    Task<string> SendAsync(string command, CancellationToken ct = default);
}

public class DockerRconService : IRconService, IDisposable
{
    private readonly DockerOptions _options;
    private readonly ILogger<DockerRconService> _logger;
    private readonly DockerClient _client;

    public DockerRconService(
        IOptions<DockerOptions> options,
        ILogger<DockerRconService> logger)
    {
        _options = options.Value;
        _logger = logger;

        var config = string.IsNullOrWhiteSpace(_options.DaemonUri)
            ? new DockerClientConfiguration()
            : new DockerClientConfiguration(new Uri(_options.DaemonUri));

        _client = config.CreateClient();
    }

    public async Task<string> SendAsync(string command, CancellationToken ct = default)
    {
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(TimeSpan.FromSeconds(_options.TimeoutSeconds));

        _logger.LogInformation("Exec in {Container}: {Cli} {Command}",
            _options.ContainerName, _options.RconCliPath, command);

        var exec = await _client.Exec.ExecCreateContainerAsync(
            _options.ContainerName,
            new ContainerExecCreateParameters
            {
                // Passed as argv, so no shell quoting concerns.
                Cmd = new List<string> { _options.RconCliPath, command },
                AttachStdout = true,
                AttachStderr = true
            },
            cts.Token);

        using var stream = await _client.Exec.StartAndAttachContainerExecAsync(
            exec.ID, tty: false, cts.Token);

        var (stdout, stderr) = await stream.ReadOutputToEndAsync(cts.Token);

        var inspect = await _client.Exec.InspectContainerExecAsync(exec.ID, cts.Token);

        if (inspect.ExitCode != 0)
        {
            _logger.LogError("rcon exec failed (exit {Code}): {Error}", inspect.ExitCode, stderr);
            throw new InvalidOperationException(
                $"'{_options.RconCliPath} {command}' exited with code {inspect.ExitCode}: {stderr}");
        }

        return stdout;
    }

    public void Dispose() => _client.Dispose();
}
