using System.Text.Json;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinRT.Interop;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Plugins;
using YMhut.Box.Core.Tools;
using YMhut.Box.WinUI.Services;

namespace YMhut.Box.WinUI.Views;

internal sealed class PluginBridge(LoadedPlugin plugin, WebView2 webView)
{
    private readonly IPluginHostProcessService _pluginHost = AppServices.GetRequiredService<IPluginHostProcessService>();
    private readonly IToolLinkNavigationService _linkNavigationService = AppServices.GetRequiredService<IToolLinkNavigationService>();
    private readonly Dictionary<string, string> _runtimeValues = new(StringComparer.OrdinalIgnoreCase);

    public string BootstrapScript => """
(() => {
  if (window.ymhut) return;
  const pending = new Map();
  let seq = 1;
  function call(method, payload) {
    const id = String(seq++);
    chrome.webview.postMessage({ id, method, payload });
    return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
  }
  window.__ymhutBridgeResolve = (message) => {
    const item = pending.get(String(message.id));
    if (!item) return;
    pending.delete(String(message.id));
    if (message.ok) item.resolve(message.value);
    else item.reject(new Error(message.error || "Plugin bridge call failed"));
  };
  chrome.webview.addEventListener("message", e => window.__ymhutBridgeResolve(e.data || {}));
  window.ymhut = {
    input: { get: () => call("input.get"), set: value => call("input.set", value), onInputChanged: handler => window.addEventListener("ymhut-input", e => handler(e.detail)) },
    output: { set: value => call("output.set", value), append: value => call("output.append", value), clear: () => call("output.clear") },
    log: { info: (message, detail) => call("log.info", { message, detail }), warn: (message, detail) => call("log.warn", { message, detail }), error: (message, detail) => call("log.error", { message, detail }) },
    storage: { get: key => call("storage.get", key), set: (key, value) => call("storage.set", { key, value }), remove: key => call("storage.remove", key), list: () => call("storage.list") },
    http: { fetch: request => call("http.fetch", request) },
    network: {
      diagnostics: () => call("network.diagnostics"),
      ping: request => call("network.ping", request),
      dnsLookup: request => call("network.dnsLookup", request),
      traceRoute: request => call("network.traceRoute", request)
    },
    clipboard: { readText: () => call("clipboard.readText"), writeText: text => call("clipboard.writeText", text) },
    file: { openPicker: () => call("file.openPicker"), savePicker: (name, value) => call("file.savePicker", { name, value }) },
    tool: { run: (toolId, input) => call("tool.run", { toolId, input }) },
    openExternal: (url, options) => call("openExternal", { url, options })
  };
})();
""";

    public event EventHandler<string>? OutputChanged;

    public void Attach()
    {
        webView.WebMessageReceived += WebView_WebMessageReceived;
    }

    public void Detach()
    {
        webView.WebMessageReceived -= WebView_WebMessageReceived;
    }

    private async void WebView_WebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
    {
        string id = string.Empty;
        try
        {
            using var document = JsonDocument.Parse(args.WebMessageAsJson);
            var root = document.RootElement;
            id = root.TryGetProperty("id", out var idElement) ? idElement.GetString() ?? string.Empty : string.Empty;
            var method = root.TryGetProperty("method", out var methodElement) ? methodElement.GetString() ?? string.Empty : string.Empty;
            var payload = root.TryGetProperty("payload", out var payloadElement) ? payloadElement : default;
            var result = await HandleAsync(method, payload).ConfigureAwait(true);
            await ReplyAsync(id, ok: true, result).ConfigureAwait(true);
        }
        catch (Exception exception)
        {
            await ReplyAsync(id, ok: false, exception.Message).ConfigureAwait(true);
        }
    }

    private async Task<object?> HandleAsync(string method, JsonElement payload)
    {
        var response = await _pluginHost.BridgeCallAsync(new PluginBridgeRequest(
            plugin.Manifest.Id,
            string.Empty,
            method,
            payload.ValueKind == JsonValueKind.Undefined ? "null" : payload.GetRawText())).ConfigureAwait(true);
        if (!response.Ok)
        {
            throw new InvalidOperationException(response.Error ?? "Plugin bridge call failed.");
        }

        var uiResult = response.UiAction switch
        {
            "clipboard.readText" => await ReadClipboardAsync().ConfigureAwait(true),
            "clipboard.writeText" => WriteClipboard(payload),
            "file.openPicker" => await OpenFilePickerAsync().ConfigureAwait(true),
            "file.savePicker" => await SaveFilePickerAsync(payload).ConfigureAwait(true),
            "openExternal" => await OpenExternalAsync(payload).ConfigureAwait(true),
            _ => method switch
            {
                "output.set" => SetOutput(payload, append: false),
                "output.append" => SetOutput(payload, append: true),
                "output.clear" => SetOutput(default, append: false, clear: true),
                _ => null
            }
        };
        if (uiResult is not null)
        {
            return uiResult;
        }

        return string.IsNullOrWhiteSpace(response.ValueJson)
            ? null
            : JsonSerializer.Deserialize<object>(response.ValueJson);
    }

    private object SetOutput(JsonElement payload, bool append, bool clear = false)
    {
        var next = clear ? string.Empty : JsonValue(payload);
        if (append && _runtimeValues.TryGetValue("output", out var current))
        {
            next = current + next;
        }

        _runtimeValues["output"] = next;
        OutputChanged?.Invoke(this, next);
        return true;
    }

    private async Task<object?> ReadClipboardAsync()
    {
        var content = Clipboard.GetContent();
        return content.Contains(StandardDataFormats.Text)
            ? await content.GetTextAsync().AsTask().ConfigureAwait(true)
            : string.Empty;
    }

    private object WriteClipboard(JsonElement payload)
    {
        var package = new DataPackage();
        package.SetText(JsonValue(payload));
        Clipboard.SetContent(package);
        return true;
    }

    private async Task<object> OpenExternalAsync(JsonElement payload)
    {
        var value = ReadString(payload, "url") ?? JsonValue(payload);
        var target = ReadString(payload, "target") ?? ReadString(payload, "options.target");
        if (payload.ValueKind == JsonValueKind.Object &&
            payload.TryGetProperty("options", out var options) &&
            options.ValueKind == JsonValueKind.Object)
        {
            target ??= ReadString(options, "target");
        }

        var linkTarget = string.Equals(target, "system", StringComparison.OrdinalIgnoreCase)
            ? ToolLinkTarget.SystemBrowser
            : ToolLinkTarget.SafeBrowser;
        await _linkNavigationService.OpenAsync(value, linkTarget).ConfigureAwait(true);
        return true;
    }

    private async Task<object?> OpenFilePickerAsync()
    {
        if (App.CurrentWindow is null)
        {
            throw new InvalidOperationException("No active window is available for the file picker.");
        }

        var picker = new FileOpenPicker();
        InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(App.CurrentWindow));
        picker.FileTypeFilter.Add("*");
        var file = await picker.PickSingleFileAsync();
        if (file is null)
        {
            return null;
        }

        var text = await FileIO.ReadTextAsync(file);
        return new { name = file.Name, content = text };
    }

    private async Task<object?> SaveFilePickerAsync(JsonElement payload)
    {
        if (App.CurrentWindow is null)
        {
            throw new InvalidOperationException("No active window is available for the file picker.");
        }

        var suggestedName = ReadString(payload, "name") ?? "plugin-output.txt";
        var value = ReadString(payload, "value") ?? ReadString(payload, "bytesOrText") ?? ReadString(payload, "text") ?? string.Empty;
        var extension = Path.GetExtension(suggestedName);
        if (string.IsNullOrWhiteSpace(extension))
        {
            extension = ".txt";
            suggestedName += extension;
        }

        var picker = new FileSavePicker { SuggestedFileName = Path.GetFileNameWithoutExtension(suggestedName) };
        InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(App.CurrentWindow));
        picker.FileTypeChoices.Add("Plugin output", [extension]);
        var file = await picker.PickSaveFileAsync();
        if (file is null)
        {
            return null;
        }

        await FileIO.WriteTextAsync(file, value);
        return new { name = file.Name };
    }

    private async Task ReplyAsync(string id, bool ok, object? valueOrError)
    {
        var payload = ok
            ? JsonSerializer.Serialize(new { id, ok = true, value = valueOrError })
            : JsonSerializer.Serialize(new { id, ok = false, error = valueOrError?.ToString() ?? "Plugin bridge call failed" });
        webView.CoreWebView2?.PostWebMessageAsJson(payload);
        await Task.CompletedTask;
    }

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

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