using System.Diagnostics;
using System.Text.Json;

namespace CodeRemoteManager.SetupApp;

public sealed class TailscaleExposureException(string code) : Exception(code);

public static class TailscaleExposurePolicy
{
    public static void Validate(string serveStatusJson, string serviceConfigJson, int backendPort)
    {
        if (backendPort is < 1 or > 65535)
            throw new TailscaleExposureException("tailscale_exposure_status_invalid");

        using var status = Parse(serveStatusJson);
        using var services = Parse(serviceConfigJson);
        if (ContainsEnabledFunnel(status.RootElement))
            throw new TailscaleExposureException("tailscale_funnel_exposed");
        if (ContainsTargetPort(status.RootElement, backendPort) ||
            ContainsTargetPort(services.RootElement, backendPort))
            throw new TailscaleExposureException("tailscale_backend_exposed");
    }

    private static JsonDocument Parse(string json)
    {
        if (string.IsNullOrWhiteSpace(json))
            throw new TailscaleExposureException("tailscale_exposure_status_invalid");
        try
        {
            return JsonDocument.Parse(json);
        }
        catch (JsonException)
        {
            throw new TailscaleExposureException("tailscale_exposure_status_invalid");
        }
    }

    private static bool ContainsEnabledFunnel(JsonElement element)
    {
        if (element.ValueKind == JsonValueKind.Object)
        {
            foreach (var property in element.EnumerateObject())
            {
                if (string.Equals(property.Name, "AllowFunnel", StringComparison.OrdinalIgnoreCase) &&
                    HasEnabledValue(property.Value))
                    return true;
                if (ContainsEnabledFunnel(property.Value)) return true;
            }
        }
        else if (element.ValueKind == JsonValueKind.Array)
        {
            foreach (var item in element.EnumerateArray())
                if (ContainsEnabledFunnel(item)) return true;
        }
        return false;
    }

    private static bool HasEnabledValue(JsonElement element) => element.ValueKind switch
    {
        JsonValueKind.True => true,
        JsonValueKind.False or JsonValueKind.Null or JsonValueKind.Undefined => false,
        JsonValueKind.String => !string.IsNullOrWhiteSpace(element.GetString()),
        JsonValueKind.Number => !element.TryGetInt64(out var value) || value != 0,
        JsonValueKind.Array => element.EnumerateArray().Any(HasEnabledValue),
        JsonValueKind.Object => element.EnumerateObject().Any(property => HasEnabledValue(property.Value)),
        _ => true
    };

    private static bool ContainsTargetPort(JsonElement element, int backendPort)
    {
        if (element.ValueKind == JsonValueKind.String)
            return TargetsPort(element.GetString(), backendPort);
        if (element.ValueKind == JsonValueKind.Object)
            return element.EnumerateObject().Any(property => ContainsTargetPort(property.Value, backendPort));
        if (element.ValueKind == JsonValueKind.Array)
            return element.EnumerateArray().Any(item => ContainsTargetPort(item, backendPort));
        return false;
    }

    private static bool TargetsPort(string? value, int backendPort)
    {
        if (string.IsNullOrWhiteSpace(value)) return false;
        if (Uri.TryCreate(value, UriKind.Absolute, out var absolute) && absolute.Port == backendPort)
            return true;
        return Uri.TryCreate($"tcp://{value}", UriKind.Absolute, out var hostPort) &&
            hostPort.Port == backendPort;
    }
}

public sealed class TailscaleExposureGuard
{
    private readonly string tailscalePath;
    private readonly Func<string, string, string> capture;

    public TailscaleExposureGuard(
        string tailscalePath,
        Func<string, string, string>? capture = null)
    {
        this.tailscalePath = tailscalePath;
        this.capture = capture ?? Capture;
    }

    public void EnsureSafe(int backendPort)
    {
        try
        {
            var status = capture(tailscalePath, "serve status --json");
            var services = capture(tailscalePath, "serve get-config --all");
            TailscaleExposurePolicy.Validate(status, services, backendPort);
        }
        catch (TailscaleExposureException)
        {
            throw;
        }
        catch
        {
            throw new TailscaleExposureException("tailscale_exposure_status_unavailable");
        }
    }

    private static string Capture(string file, string arguments)
    {
        using var process = Process.Start(new ProcessStartInfo(file, arguments)
        {
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        }) ?? throw new InvalidOperationException();
        var output = process.StandardOutput.ReadToEndAsync();
        var error = process.StandardError.ReadToEndAsync();
        if (!process.WaitForExit(10_000))
        {
            process.Kill(entireProcessTree: true);
            throw new TimeoutException();
        }
        Task.WaitAll(output, error);
        if (process.ExitCode != 0 || output.Result.Length > 1_048_576)
            throw new InvalidOperationException();
        return output.Result;
    }
}
