using System.Collections.Concurrent;
using System.Globalization;
using System.IO.Pipes;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using YMhut.Box.Core.Api;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Data;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Plugins;
using YMhut.Box.Core.Settings;
using YMhut.Box.Core.Tools;

Console.OutputEncoding = Encoding.UTF8;

var pipeName = ReadPipeName(args);
if (string.IsNullOrWhiteSpace(pipeName))
{
    Console.Error.WriteLine("Missing --pipe argument.");
    return 2;
}

var appPaths = AppPaths.ForCurrentUser();
var logService = new SqliteLogService(appPaths);
var settingsService = new AppSettingsService(new AppSettingsStore(appPaths.Root));
await settingsService.LoadAsync().ConfigureAwait(false);

using var httpService = new HttpService(logService: logService, settingsService: settingsService);
var apiManager = new ApiManager(httpService, logService);
var referenceDataService = new ReferenceDataService(appPaths);
var stateStore = new PluginStateStore(appPaths);
var builtInPluginInstaller = new BuiltInPluginInstallerService(appPaths, logService, settingsService);
var registry = new PluginRegistryService(appPaths, stateStore, logService, settingsService, builtInPluginInstaller);
var writeGate = new SemaphoreSlim(1, 1);
var snapshotGate = new SemaphoreSlim(1, 1);
var runtimeValues = new ConcurrentDictionary<string, ConcurrentDictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
FileSystemWatcher? watcher = null;
CancellationTokenSource? reloadDebounce = null;
PluginSnapshot? currentSnapshot = null;
var backgroundRefreshActive = 0;

await using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await pipe.ConnectAsync(8000).ConfigureAwait(false);
using var reader = new StreamReader(pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true);
await using var writer = new StreamWriter(pipe, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 4096, leaveOpen: true)
{
    AutoFlush = true
};

await WriteAsync(new PluginHostMessage(PluginHostProtocol.Ready, Version: PluginHostProtocol.Version), CancellationToken.None)
    .ConfigureAwait(false);
await logService.WriteAsync("Information", "plugin-host", "Plugin host ready").ConfigureAwait(false);
ConfigureWatcher();

while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line)
{
    var message = PluginHostProtocol.Deserialize(line);
    if (message is null)
    {
        continue;
    }

    if (string.Equals(message.Type, PluginHostProtocol.Shutdown, StringComparison.Ordinal))
    {
        break;
    }

    _ = Task.Run(async () => await HandleAsync(message).ConfigureAwait(false));
}

watcher?.Dispose();
reloadDebounce?.Cancel();
reloadDebounce?.Dispose();
return 0;

async Task HandleAsync(PluginHostMessage message)
{
    try
    {
        switch (message.Type)
        {
            case PluginHostProtocol.Ping:
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.Pong, message.RequestId), CancellationToken.None).ConfigureAwait(false);
                break;
            case PluginHostProtocol.GetSnapshot:
                var hadSnapshot = currentSnapshot is not null;
                await EnsureSnapshotAvailableAsync(CancellationToken.None).ConfigureAwait(false);
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.GetSnapshot, message.RequestId, Snapshot: currentSnapshot), CancellationToken.None).ConfigureAwait(false);
                if (hadSnapshot)
                {
                    QueueSnapshotRefresh();
                }

                break;
            case PluginHostProtocol.Reload:
                await settingsService.LoadAsync().ConfigureAwait(false);
                await ReloadSnapshotAsync(broadcast: true, CancellationToken.None).ConfigureAwait(false);
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.Reload, message.RequestId, Snapshot: currentSnapshot), CancellationToken.None).ConfigureAwait(false);
                break;
            case PluginHostProtocol.SetPluginEnabled:
                await stateStore.SetEnabledAsync(Required(message.PluginId), message.Enabled == true).ConfigureAwait(false);
                await ReloadSnapshotAsync(broadcast: true, CancellationToken.None).ConfigureAwait(false);
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.SetPluginEnabled, message.RequestId, Snapshot: currentSnapshot), CancellationToken.None).ConfigureAwait(false);
                break;
            case PluginHostProtocol.SetPermission:
                await stateStore.SetPermissionAsync(Required(message.PluginId), message.Permission ?? throw new InvalidOperationException("Missing permission."), message.Granted == true).ConfigureAwait(false);
                await ReloadSnapshotAsync(broadcast: true, CancellationToken.None).ConfigureAwait(false);
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.SetPermission, message.RequestId, Snapshot: currentSnapshot), CancellationToken.None).ConfigureAwait(false);
                break;
            case PluginHostProtocol.SetSurfaceMounted:
                await stateStore.SetSurfaceMountedAsync(Required(message.PluginId), Required(message.SurfaceId), message.Mounted == true).ConfigureAwait(false);
                await ReloadSnapshotAsync(broadcast: true, CancellationToken.None).ConfigureAwait(false);
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.SetSurfaceMounted, message.RequestId, Snapshot: currentSnapshot), CancellationToken.None).ConfigureAwait(false);
                break;
            case PluginHostProtocol.BridgeCall:
                var response = await HandleBridgeAsync(message.BridgeRequest ?? throw new InvalidOperationException("Missing bridge request.")).ConfigureAwait(false);
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.BridgeCall, message.RequestId, BridgeResponse: response), CancellationToken.None).ConfigureAwait(false);
                break;
            default:
                await WriteAsync(new PluginHostMessage(PluginHostProtocol.Error, message.RequestId, Error: $"Unknown message type: {message.Type}"), CancellationToken.None).ConfigureAwait(false);
                break;
        }
    }
    catch (Exception exception)
    {
        await logService.WriteAsync("Error", "plugin-host", "Plugin host request failed", exception.Message).ConfigureAwait(false);
        await WriteAsync(new PluginHostMessage(PluginHostProtocol.Error, message.RequestId, Error: exception.Message), CancellationToken.None).ConfigureAwait(false);
    }
}

async Task EnsureSnapshotAvailableAsync(CancellationToken cancellationToken)
{
    if (currentSnapshot is not null)
    {
        return;
    }

    await settingsService.LoadAsync(cancellationToken).ConfigureAwait(false);
    await ReloadSnapshotAsync(broadcast: false, cancellationToken).ConfigureAwait(false);
}

void QueueSnapshotRefresh()
{
    if (Interlocked.Exchange(ref backgroundRefreshActive, 1) == 1)
    {
        return;
    }

    _ = Task.Run(async () =>
    {
        try
        {
            await settingsService.LoadAsync().ConfigureAwait(false);
            await ReloadSnapshotAsync(broadcast: true, CancellationToken.None).ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            await logService.WriteAsync("Warning", "plugin-host", "Plugin background refresh failed", exception.Message).ConfigureAwait(false);
        }
        finally
        {
            Interlocked.Exchange(ref backgroundRefreshActive, 0);
        }
    });
}

async Task ReloadSnapshotAsync(bool broadcast, CancellationToken cancellationToken)
{
    await snapshotGate.WaitAsync(cancellationToken).ConfigureAwait(false);
    try
    {
        var plugins = await registry.LoadPluginsAsync(cancellationToken).ConfigureAwait(false);
        var tools = settingsService.Current.PluginsEnabled
            ? plugins
                .Where(plugin => plugin.IsValid && plugin.State.Enabled)
                .SelectMany(plugin => plugin.Manifest.Surfaces
                    .Where(surface => surface.Kind == PluginSurfaceKind.ToolboxTool &&
                        (plugin.State.MountedSurfaceIds.Count == 0 || plugin.State.MountedSurfaceIds.Contains(surface.Id)))
                    .Select(surface => new PluginToolModule(plugin, surface)))
                .ToArray()
            : [];
        currentSnapshot = new PluginSnapshot(
            settingsService.Current.PluginsEnabled,
            registry.PluginsRoot,
            plugins.Select(LoadedPluginDto.FromLoadedPlugin).ToArray(),
            tools.Select(PluginToolDto.FromPluginToolModule).ToArray(),
            DateTimeOffset.Now);
        ConfigureWatcher();
    }
    finally
    {
        snapshotGate.Release();
    }

    if (broadcast && currentSnapshot is not null)
    {
        await WriteAsync(new PluginHostMessage(PluginHostProtocol.SnapshotChanged, Snapshot: currentSnapshot), CancellationToken.None).ConfigureAwait(false);
    }
}

void ConfigureWatcher()
{
    if (!settingsService.Current.PluginsEnabled)
    {
        watcher?.Dispose();
        watcher = null;
        return;
    }

    Directory.CreateDirectory(registry.PluginsRoot);
    if (watcher is not null && string.Equals(watcher.Path, registry.PluginsRoot, StringComparison.OrdinalIgnoreCase))
    {
        return;
    }

    watcher?.Dispose();
    watcher = new FileSystemWatcher(registry.PluginsRoot)
    {
        IncludeSubdirectories = true,
        NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.Size,
        EnableRaisingEvents = true
    };
    watcher.Created += PluginFilesChanged;
    watcher.Changed += PluginFilesChanged;
    watcher.Deleted += PluginFilesChanged;
    watcher.Renamed += PluginFilesChanged;
    watcher.Error += (_, e) => _ = logService.WriteAsync("Warning", "plugin-host", "Plugin watcher failed", e.GetException().Message);
}

void PluginFilesChanged(object sender, FileSystemEventArgs e)
{
    reloadDebounce?.Cancel();
    reloadDebounce?.Dispose();
    var cts = new CancellationTokenSource();
    reloadDebounce = cts;
    _ = Task.Run(async () =>
    {
        try
        {
            await Task.Delay(500, cts.Token).ConfigureAwait(false);
            await settingsService.LoadAsync(cts.Token).ConfigureAwait(false);
            await ReloadSnapshotAsync(broadcast: true, cts.Token).ConfigureAwait(false);
            await logService.WriteAsync("Information", "plugin-host", "Plugin hot reload", e.Name).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception exception)
        {
            await logService.WriteAsync("Error", "plugin-host", "Plugin hot reload failed", exception.Message).ConfigureAwait(false);
        }
    });
}

async Task<PluginBridgeResponse> HandleBridgeAsync(PluginBridgeRequest request)
{
    var plugin = await FindPluginAsync(request.PluginId).ConfigureAwait(false);
    if (plugin is null || !plugin.IsValid || !plugin.State.Enabled)
    {
        return Fail("Plugin is not enabled or valid.");
    }

    try
    {
        using var document = string.IsNullOrWhiteSpace(request.PayloadJson)
            ? JsonDocument.Parse("null")
            : JsonDocument.Parse(request.PayloadJson);
        var payload = document.RootElement;
        return request.Method switch
        {
            "input.get" => JsonOk(GetRuntime(plugin.Manifest.Id, "input", "{}")),
            "input.set" => SetRuntime(plugin.Manifest.Id, "input", JsonValue(payload), PluginPermission.Input),
            "output.set" => AuthorizeUi(plugin, PluginPermission.Output),
            "output.append" => AuthorizeUi(plugin, PluginPermission.Output),
            "output.clear" => AuthorizeUi(plugin, PluginPermission.Output),
            "log.info" => await LogAsync(plugin, "Information", payload).ConfigureAwait(false),
            "log.warn" => await LogAsync(plugin, "Warning", payload).ConfigureAwait(false),
            "log.error" => await LogAsync(plugin, "Error", payload).ConfigureAwait(false),
            "storage.get" => await GetStorageAsync(plugin, payload).ConfigureAwait(false),
            "storage.set" => await SetStorageAsync(plugin, payload).ConfigureAwait(false),
            "storage.remove" => await RemoveStorageAsync(plugin, payload).ConfigureAwait(false),
            "storage.list" => await ListStorageAsync(plugin).ConfigureAwait(false),
            "http.fetch" => await FetchAsync(plugin, payload).ConfigureAwait(false),
            "network.ping" => await PingAsync(plugin, payload).ConfigureAwait(false),
            "network.dnsLookup" => await DnsLookupAsync(plugin, payload).ConfigureAwait(false),
            "network.diagnostics" => NetworkDiagnostics(plugin),
            "network.traceRoute" => await TraceRouteAsync(plugin, payload).ConfigureAwait(false),
            "tool.run" => await RunToolAsync(plugin, payload).ConfigureAwait(false),
            "clipboard.readText" => AuthorizeUi(plugin, PluginPermission.Clipboard, "clipboard.readText"),
            "clipboard.writeText" => AuthorizeUi(plugin, PluginPermission.Clipboard, "clipboard.writeText"),
            "file.openPicker" => AuthorizeUi(plugin, PluginPermission.FilePicker, "file.openPicker"),
            "file.savePicker" => AuthorizeUi(plugin, PluginPermission.FilePicker, "file.savePicker"),
            "openExternal" => ValidateExternal(plugin, payload),
            _ => Fail($"Unknown plugin bridge method: {request.Method}")
        };
    }
    catch (Exception exception)
    {
        return Fail(exception.Message);
    }
}

async Task<LoadedPlugin?> FindPluginAsync(string pluginId)
{
    var snapshot = currentSnapshot;
    if (snapshot is null)
    {
        await ReloadSnapshotAsync(broadcast: false, CancellationToken.None).ConfigureAwait(false);
        snapshot = currentSnapshot;
    }

    return snapshot?.Plugins
        .FirstOrDefault(plugin => string.Equals(plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
        ?.ToLoadedPlugin();
}

PluginBridgeResponse SetRuntime(string pluginId, string key, string value, PluginPermission permission)
{
    EnsurePermissionById(pluginId, permission);
    Runtime(pluginId)[key] = value;
    return JsonOk(true);
}

PluginBridgeResponse AuthorizeUi(LoadedPlugin plugin, PluginPermission permission, string? uiAction = null)
{
    EnsurePluginPermission(plugin, permission);
    return new PluginBridgeResponse(true, JsonSerializer.Serialize(true), UiAction: uiAction);
}

PluginBridgeResponse ValidateExternal(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.OpenExternal);
    var value = payload.ValueKind == JsonValueKind.Object
        ? ReadString(payload, "url") ?? ReadString(payload, "uri") ?? string.Empty
        : JsonValue(payload);
    if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) ||
        (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
    {
        return Fail("Only absolute http/https URLs can be opened externally.");
    }

    return new PluginBridgeResponse(true, JsonSerializer.Serialize(true), UiAction: "openExternal");
}

async Task<PluginBridgeResponse> LogAsync(LoadedPlugin plugin, string level, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.Log);
    var message = ReadString(payload, "message") ?? JsonValue(payload);
    var detail = ReadString(payload, "detail");
    await logService.WriteAsync(level, $"plugin:{plugin.Manifest.Id}", message, detail).ConfigureAwait(false);
    return JsonOk(true);
}

async Task<PluginBridgeResponse> GetStorageAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.Storage);
    var value = await stateStore.GetValueAsync(plugin.Manifest.Id, JsonValue(payload)).ConfigureAwait(false);
    return JsonOk(value);
}

async Task<PluginBridgeResponse> SetStorageAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.Storage);
    var key = ReadString(payload, "key") ?? throw new InvalidOperationException("storage.set requires key.");
    var value = ReadString(payload, "value") ?? JsonValue(payload.GetProperty("value"));
    await stateStore.SetValueAsync(plugin.Manifest.Id, key, value).ConfigureAwait(false);
    return JsonOk(true);
}

async Task<PluginBridgeResponse> RemoveStorageAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.Storage);
    await stateStore.RemoveValueAsync(plugin.Manifest.Id, JsonValue(payload)).ConfigureAwait(false);
    return JsonOk(true);
}

async Task<PluginBridgeResponse> ListStorageAsync(LoadedPlugin plugin)
{
    EnsurePluginPermission(plugin, PluginPermission.Storage);
    return JsonOk(await stateStore.ListValuesAsync(plugin.Manifest.Id).ConfigureAwait(false));
}

async Task<PluginBridgeResponse> FetchAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.Http);
    var rawUrl = payload.ValueKind == JsonValueKind.Object ? ReadString(payload, "url") ?? ReadString(payload, "uri") : JsonValue(payload);
    if (!Uri.TryCreate(rawUrl, UriKind.Absolute, out var uri) ||
        (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
    {
        return Fail("ymhut.http.fetch only accepts absolute http/https URLs.");
    }

    var method = payload.ValueKind == JsonValueKind.Object ? ReadString(payload, "method") ?? "GET" : "GET";
    var body = payload.ValueKind == JsonValueKind.Object ? ReadString(payload, "body") : null;
    var headers = ReadHeaders(payload);
    var result = await httpService.SendAsync(uri, method, body, headers, ensureSuccess: false).ConfigureAwait(false);
    await logService.WriteAsync("Information", $"plugin:{plugin.Manifest.Id}", "Plugin HTTP fetch", uri.Host).ConfigureAwait(false);
    return JsonOk(new
    {
        status = (int)result.StatusCode,
        ok = (int)result.StatusCode is >= 200 and < 300,
        content = result.Content,
        headers = result.Headers,
        elapsedMs = (long)result.Elapsed.TotalMilliseconds
    });
}

async Task<PluginBridgeResponse> PingAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.NetworkDiagnostics);
    var host = payload.ValueKind == JsonValueKind.Object ? ReadString(payload, "host") : JsonValue(payload);
    if (string.IsNullOrWhiteSpace(host))
    {
        return Fail("network.ping requires host.");
    }

    var count = payload.ValueKind == JsonValueKind.Object ? ReadInt(payload, "count", 4, 1, 12) : 4;
    var timeout = payload.ValueKind == JsonValueKind.Object ? ReadInt(payload, "timeoutMs", 2500, 500, 10000) : 2500;
    var rows = new List<object>();
    using var ping = new Ping();
    for (var index = 0; index < count; index++)
    {
        try
        {
            var reply = await ping.SendPingAsync(host.Trim(), timeout).ConfigureAwait(false);
            rows.Add(new
            {
                seq = index + 1,
                status = reply.Status.ToString(),
                address = reply.Address?.ToString() ?? string.Empty,
                roundtripMs = reply.Status == IPStatus.Success ? reply.RoundtripTime : -1
            });
        }
        catch (Exception exception)
        {
            rows.Add(new { seq = index + 1, status = "Error", address = string.Empty, roundtripMs = -1, error = exception.Message });
        }
    }

    var success = rows
        .Select(item => JsonSerializer.SerializeToElement(item))
        .Where(item => string.Equals(ReadString(item, "status"), IPStatus.Success.ToString(), StringComparison.OrdinalIgnoreCase))
        .Select(item => (double)ReadLong(item, "roundtripMs", -1))
        .Where(value => value >= 0)
        .ToArray();
    return JsonOk(new
    {
        host = host.Trim(),
        count,
        sent = count,
        received = success.Length,
        lossPercent = Math.Round((count - success.Length) * 100d / count, 1),
        minMs = success.Length == 0 ? -1 : Math.Round(success.Min(), 1),
        avgMs = success.Length == 0 ? -1 : Math.Round(success.Average(), 1),
        maxMs = success.Length == 0 ? -1 : Math.Round(success.Max(), 1),
        replies = rows
    });
}

async Task<PluginBridgeResponse> DnsLookupAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.NetworkDiagnostics);
    var host = payload.ValueKind == JsonValueKind.Object ? ReadString(payload, "host") : JsonValue(payload);
    if (string.IsNullOrWhiteSpace(host))
    {
        return Fail("network.dnsLookup requires host.");
    }

    try
    {
        var started = DateTimeOffset.UtcNow;
        var entries = await Dns.GetHostAddressesAsync(host.Trim()).ConfigureAwait(false);
        return JsonOk(new
        {
            host = host.Trim(),
            elapsedMs = (long)(DateTimeOffset.UtcNow - started).TotalMilliseconds,
            addresses = entries.Select(address => new
            {
                value = address.ToString(),
                family = address.AddressFamily.ToString()
            }).ToArray()
        });
    }
    catch (Exception exception)
    {
        return Fail(exception.Message);
    }
}

async Task<PluginBridgeResponse> TraceRouteAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.NetworkDiagnostics);
    var host = payload.ValueKind == JsonValueKind.Object ? ReadString(payload, "host") : JsonValue(payload);
    if (string.IsNullOrWhiteSpace(host))
    {
        return Fail("network.traceRoute requires host.");
    }

    var maxHops = payload.ValueKind == JsonValueKind.Object ? ReadInt(payload, "maxHops", 12, 1, 30) : 12;
    var timeout = payload.ValueKind == JsonValueKind.Object ? ReadInt(payload, "timeoutMs", 2200, 500, 8000) : 2200;
    var hops = new List<object>();
    var buffer = Encoding.ASCII.GetBytes("ymhut");
    using var ping = new Ping();
    for (var ttl = 1; ttl <= maxHops; ttl++)
    {
        try
        {
            var options = new PingOptions(ttl, dontFragment: true);
            var reply = await ping.SendPingAsync(host.Trim(), timeout, buffer, options).ConfigureAwait(false);
            hops.Add(new
            {
                ttl,
                status = reply.Status.ToString(),
                address = reply.Address?.ToString() ?? "*",
                roundtripMs = reply.Status is IPStatus.Success or IPStatus.TtlExpired ? reply.RoundtripTime : -1
            });
            if (reply.Status == IPStatus.Success)
            {
                break;
            }
        }
        catch (Exception exception)
        {
            hops.Add(new { ttl, status = "Error", address = "*", roundtripMs = -1, error = exception.Message });
        }
    }

    return JsonOk(new { host = host.Trim(), maxHops, hops });
}

PluginBridgeResponse NetworkDiagnostics(LoadedPlugin plugin)
{
    EnsurePluginPermission(plugin, PluginPermission.NetworkDiagnostics);
    var settings = settingsService.Current;
    var interfaces = NetworkInterface.GetAllNetworkInterfaces()
        .Where(item => item.NetworkInterfaceType != NetworkInterfaceType.Loopback)
        .Select(item =>
        {
            var properties = item.GetIPProperties();
            var statistics = item.GetIPv4Statistics();
            return new
            {
                id = item.Id,
                name = item.Name,
                description = item.Description,
                type = item.NetworkInterfaceType.ToString(),
                status = item.OperationalStatus.ToString(),
                speedMbps = item.Speed > 0 ? Math.Round(item.Speed / 1000d / 1000d, 1) : 0,
                dnsSuffix = properties.DnsSuffix,
                ipv4 = properties.UnicastAddresses
                    .Where(address => address.Address.AddressFamily == AddressFamily.InterNetwork)
                    .Select(address => address.Address.ToString())
                    .ToArray(),
                ipv6 = properties.UnicastAddresses
                    .Where(address => address.Address.AddressFamily == AddressFamily.InterNetworkV6)
                    .Select(address => address.Address.ToString())
                    .ToArray(),
                gateways = properties.GatewayAddresses.Select(address => address.Address.ToString()).ToArray(),
                dnsServers = properties.DnsAddresses.Select(address => address.ToString()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
                bytesReceived = statistics.BytesReceived,
                bytesSent = statistics.BytesSent
            };
        })
        .ToArray();

    var active = interfaces.Where(item => string.Equals(item.status, "Up", StringComparison.OrdinalIgnoreCase)).ToArray();
    var dnsServers = active.SelectMany(item => item.dnsServers).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
    return JsonOk(new
    {
        generatedAt = DateTimeOffset.Now,
        machine = Environment.MachineName,
        os = Environment.OSVersion.VersionString,
        processArchitecture = RuntimeInformation.ProcessArchitecture.ToString(),
        uiCulture = CultureInfo.CurrentUICulture.Name,
        localTimeZone = TimeZoneInfo.Local.Id,
        proxy = new
        {
            enabled = settings.ProxyEnabled,
            mode = settings.ProxyMode,
            host = settings.ProxyEnabled ? settings.ProxyHost : string.Empty,
            port = settings.ProxyEnabled ? settings.ProxyPort : 0
        },
        interfaces,
        summary = new
        {
            interfaceCount = interfaces.Length,
            activeInterfaceCount = active.Length,
            ipv4Count = active.Sum(item => item.ipv4.Length),
            ipv6Count = active.Sum(item => item.ipv6.Length),
            dnsServers,
            defaultGateways = active.SelectMany(item => item.gateways).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
        },
        offlineMode = true,
        note = "Local diagnostics only. Public IP, ASN, external DNS leak, and internet speed require a remote observer and are intentionally not fetched by the built-in sample."
    });
}

async Task<PluginBridgeResponse> RunToolAsync(LoadedPlugin plugin, JsonElement payload)
{
    EnsurePluginPermission(plugin, PluginPermission.RunTool);
    var toolId = ReadString(payload, "toolId") ?? throw new InvalidOperationException("ymhut.tool.run requires toolId.");
    var input = ReadString(payload, "input") ?? string.Empty;
    if (PluginIds.IsPluginToolId(toolId))
    {
        return Fail("Plugins cannot call plugin tools through ymhut.tool.run.");
    }

    var catalog = new ToolCatalog();
    var module = catalog.GetById(toolId) ?? throw new InvalidOperationException($"Tool was not found: {toolId}");
    if (!module.Metadata.OfflineCapable)
    {
        return Fail("ymhut.tool.run v1 only allows offline built-in tools.");
    }

    var result = await ToolExecutor.ExecuteAsync(module, input, apiManager: apiManager, referenceDataService: referenceDataService).ConfigureAwait(false);
    await logService.WriteAsync("Information", $"plugin:{plugin.Manifest.Id}", "Plugin ran built-in tool", toolId).ConfigureAwait(false);
    return JsonOk(new { ok = result.Ok, output = result.Output, error = result.Error });
}

void EnsurePermissionById(string pluginId, PluginPermission permission)
{
    var plugin = currentSnapshot?.Plugins.FirstOrDefault(item => string.Equals(item.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))?.ToLoadedPlugin()
        ?? throw new UnauthorizedAccessException("Plugin is not loaded.");
    EnsurePluginPermission(plugin, permission);
}

static void EnsurePluginPermission(LoadedPlugin plugin, PluginPermission permission)
{
    if (!plugin.State.GrantedPermissions.Contains(permission))
    {
        throw new UnauthorizedAccessException($"Plugin permission is not granted: {permission}");
    }
}

ConcurrentDictionary<string, string> Runtime(string pluginId)
{
    return runtimeValues.GetOrAdd(pluginId, _ => new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase));
}

string GetRuntime(string pluginId, string key, string fallback)
{
    return Runtime(pluginId).TryGetValue(key, out var value) ? value : fallback;
}

static PluginBridgeResponse JsonOk(object? value)
{
    return new PluginBridgeResponse(true, JsonSerializer.Serialize(value));
}

static PluginBridgeResponse Fail(string error)
{
    return new PluginBridgeResponse(false, Error: error);
}

async Task WriteAsync(PluginHostMessage message, CancellationToken cancellationToken)
{
    await writeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
    try
    {
        await writer.WriteLineAsync(PluginHostProtocol.Serialize(message).AsMemory(), cancellationToken).ConfigureAwait(false);
        await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
    }
    finally
    {
        writeGate.Release();
    }
}

static string Required(string? value)
{
    return string.IsNullOrWhiteSpace(value) ? throw new InvalidOperationException("Missing required value.") : value;
}

static string JsonValue(JsonElement element)
{
    return element.ValueKind == JsonValueKind.String ? element.GetString() ?? string.Empty : element.GetRawText();
}

static string? ReadString(JsonElement element, string property)
{
    return element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out var value) ? JsonValue(value) : null;
}

static IReadOnlyDictionary<string, string>? ReadHeaders(JsonElement payload)
{
    if (payload.ValueKind != JsonValueKind.Object ||
        !payload.TryGetProperty("headers", out var headersElement) ||
        headersElement.ValueKind != JsonValueKind.Object)
    {
        return null;
    }

    var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    foreach (var property in headersElement.EnumerateObject())
    {
        headers[property.Name] = property.Value.ValueKind == JsonValueKind.String
            ? property.Value.GetString() ?? string.Empty
            : property.Value.GetRawText();
    }

    return headers;
}

static int ReadInt(JsonElement element, string property, int fallback, int min, int max)
{
    if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(property, out var value))
    {
        return fallback;
    }

    var parsed = value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number)
        ? number
        : int.TryParse(JsonValue(value), out var textNumber)
            ? textNumber
            : fallback;
    return Math.Clamp(parsed, min, max);
}

static long ReadLong(JsonElement element, string property, long fallback)
{
    if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(property, out var value))
    {
        return fallback;
    }

    return value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number)
        ? number
        : long.TryParse(JsonValue(value), out var textNumber)
            ? textNumber
            : fallback;
}

static string? ReadPipeName(string[] args)
{
    for (var index = 0; index < args.Length; index++)
    {
        if (string.Equals(args[index], "--pipe", StringComparison.OrdinalIgnoreCase) && index + 1 < args.Length)
        {
            return args[index + 1];
        }
    }

    return null;
}
