using AnsonBot.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Net.Sockets;

namespace AnsonBot.Bot;

/// <summary>
/// Development-only. Holds open an SSH tunnel from a local TCP port to the
/// home server's Docker socket, so DockerRconService can reach the daemon.
/// </summary>
public class SshTunnelService : IHostedService
{
    private readonly SshTunnelOptions _options;
    private readonly ILogger<SshTunnelService> _logger;
    private Process? _ssh;

    public SshTunnelService(
        IOptions<SshTunnelOptions> options,
        ILogger<SshTunnelService> logger)
    {
        _options = options.Value;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken ct)
    {
        // A tunnel may already be up from a previous debug session that was
        // killed hard (Stop Debugging doesn't always run StopAsync).
        if (await IsPortOpenAsync(_options.LocalPort, ct))
        {
            _logger.LogInformation(
                "Port {Port} already open; assuming an existing tunnel.", _options.LocalPort);
            return;
        }

        var args = $"-N -o ExitOnForwardFailure=yes -o BatchMode=yes " +
                   $"-L {_options.LocalPort}:{_options.RemoteSocket} " +
                   $"{_options.User}@{_options.Host}";

        _logger.LogInformation("Starting SSH tunnel: ssh {Args}", args);

        _ssh = Process.Start(new ProcessStartInfo("ssh", args)
        {
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        }) ?? throw new InvalidOperationException("Could not start ssh. Is it on PATH?");

        // Don't let the bot start talking to Docker before the forward is live.
        for (var i = 0; i < _options.StartupTimeoutSeconds * 10; i++)
        {
            if (_ssh.HasExited)
            {
                var err = await _ssh.StandardError.ReadToEndAsync(ct);
                throw new InvalidOperationException($"SSH tunnel failed to start: {err}");
            }

            if (await IsPortOpenAsync(_options.LocalPort, ct))
            {
                _logger.LogInformation("SSH tunnel is up on port {Port}.", _options.LocalPort);
                return;
            }

            await Task.Delay(100, ct);
        }

        throw new TimeoutException(
            $"SSH tunnel did not open port {_options.LocalPort} within " +
            $"{_options.StartupTimeoutSeconds}s.");
    }

    public Task StopAsync(CancellationToken ct)
    {
        if (_ssh is { HasExited: false })
        {
            _logger.LogInformation("Closing SSH tunnel.");
            _ssh.Kill(entireProcessTree: true);
        }

        _ssh?.Dispose();
        _ssh = null;
        return Task.CompletedTask;
    }

    private static async Task<bool> IsPortOpenAsync(int port, CancellationToken ct)
    {
        try
        {
            using var client = new TcpClient();
            await client.ConnectAsync("localhost", port, ct);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

public class SshTunnelOptions
{
    public string Host { get; set; } = "192.168.40.100";
    public string User { get; set; } = "sam";
    public int LocalPort { get; set; } = 2375;
    public string RemoteSocket { get; set; } = "/var/run/docker.sock";
    public int StartupTimeoutSeconds { get; set; } = 15;
}
