using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Nexus.Api.Models;

namespace Nexus.Api.Services;

/// <summary>
/// Secret-safe projection of OpenClaw's official gateway-driven onboarding
/// wizard. Nexus renders the protocol; it does not reimplement onboarding,
/// install packages, or accept provider credentials through the browser.
/// </summary>
public sealed class OpenClawWizardService(
    IGatewayConnector connector,
    ILogger<OpenClawWizardService> logger,
    IOpenClawManagementState? managementState = null) : IOpenClawWizardService
{
    private const int MaxAnswerBytes = 64 * 1024;
    private readonly ConcurrentDictionary<string, WizardSessionState> sessions =
        new(StringComparer.Ordinal);

    public async Task<OpenClawWizardResultDto> StartAsync(
        StartOpenClawWizardRequest request,
        OpenClawInvocationContext invocation,
        CancellationToken cancellationToken = default)
    {
        if (!request.Confirmed)
        {
            return Failure(
                "confirmation_required",
                "Der OpenClaw-Assistent wurde nicht gestartet.",
                "Die schreibende OpenClaw-Einrichtung muss ausdrücklich bestätigt werden.");
        }

        var mode = request.Mode.Trim().ToLowerInvariant();
        if (mode is not ("local" or "remote"))
        {
            return Failure(
                "invalid",
                "Der OpenClaw-Assistent wurde nicht gestartet.",
                "mode muss local oder remote sein.");
        }

        var unavailable = CheckAvailability("wizard.start");
        if (unavailable is not null)
            return unavailable;

        try
        {
            var response = await connector.InvokeAsync(
                "wizard.start",
                new JsonObject
                {
                    ["mode"] = mode,
                    ["installDaemon"] = false,
                    ["flow"] = "setup"
                },
                cancellationToken: cancellationToken,
                invocationContext: invocation with { IncludeIdempotencyParameter = false });
            return MapResult(response, sessionId: null);
        }
        catch (Exception exception)
        {
            logger.LogWarning(exception, "OpenClaw wizard start failed");
            return GatewayFailure(exception);
        }
    }

    public async Task<OpenClawWizardResultDto> NextAsync(
        AdvanceOpenClawWizardRequest request,
        OpenClawInvocationContext invocation,
        CancellationToken cancellationToken = default)
    {
        var sessionId = NormalizeSessionId(request.SessionId);
        if (sessionId is null || !sessions.TryGetValue(sessionId, out var current))
        {
            return Failure(
                "not_found",
                "Die OpenClaw-Assistentensitzung ist nicht mehr verfügbar.",
                "Assistent neu starten; abgeschlossene Sitzungen werden von OpenClaw entfernt.",
                sessionId);
        }

        var unavailable = CheckAvailability("wizard.next");
        if (unavailable is not null)
            return unavailable with { SessionId = sessionId };

        JsonObject? answer = null;
        if (request.HasAnswer)
        {
            var stepId = request.StepId?.Trim();
            if (string.IsNullOrWhiteSpace(stepId) ||
                !string.Equals(stepId, current.StepId, StringComparison.Ordinal))
            {
                return Failure(
                    "conflict",
                    "Die Antwort gehört nicht zum aktuellen OpenClaw-Schritt.",
                    "Aktuellen Schritt neu laden und erneut antworten.",
                    sessionId);
            }
            if (current.Sensitive)
            {
                return Failure(
                    "server_secret_required",
                    "Dieser OpenClaw-Schritt erwartet ein Geheimnis.",
                    "Provider-Secret serverseitig als SecretRef bereitstellen und den offiziellen OpenClaw-Flow dort fortsetzen. Nexus nimmt keine Provider-Secrets aus dem Browser an.",
                    sessionId);
            }

            if (Encoding.UTF8.GetByteCount(request.Value?.ToJsonString() ?? "null") > MaxAnswerBytes)
            {
                return Failure(
                    "invalid",
                    "Die OpenClaw-Antwort ist zu groß.",
                    "Antwort auf höchstens 64 KiB reduzieren.",
                    sessionId);
            }

            answer = new JsonObject
            {
                ["stepId"] = stepId,
                ["value"] = request.Value?.DeepClone()
            };
        }

        try
        {
            var parameters = new JsonObject { ["sessionId"] = sessionId };
            if (answer is not null)
                parameters["answer"] = answer;
            var response = await connector.InvokeAsync(
                "wizard.next",
                parameters,
                cancellationToken: cancellationToken,
                invocationContext: invocation with { IncludeIdempotencyParameter = false });
            return MapResult(response, sessionId);
        }
        catch (Exception exception)
        {
            logger.LogWarning(exception, "OpenClaw wizard next failed");
            return GatewayFailure(exception, sessionId);
        }
    }

    public async Task<OpenClawWizardResultDto> GetStatusAsync(
        string sessionId,
        CancellationToken cancellationToken = default)
    {
        var normalized = NormalizeSessionId(sessionId);
        if (normalized is null || !sessions.ContainsKey(normalized))
        {
            return Failure(
                "not_found",
                "Die OpenClaw-Assistentensitzung ist nicht mehr verfügbar.",
                "Assistent neu starten.",
                normalized);
        }

        var unavailable = CheckAvailability("wizard.status");
        if (unavailable is not null)
            return unavailable with { SessionId = normalized };

        try
        {
            var response = await connector.InvokeAsync(
                "wizard.status",
                new JsonObject { ["sessionId"] = normalized },
                cancellationToken: cancellationToken);
            var status = SafeString(response?["status"], 40);
            var error = SafeString(response?["error"], 1000);
            if (status is not "running")
                sessions.TryRemove(normalized, out _);
            return new OpenClawWizardResultDto(
                true,
                status ?? "running",
                status == "running"
                    ? "Der OpenClaw-Assistent läuft."
                    : "Der OpenClaw-Assistent ist beendet.",
                normalized,
                status is "done" or "cancelled" or "error",
                status,
                error,
                null,
                error,
                DateTimeOffset.UtcNow);
        }
        catch (Exception exception)
        {
            logger.LogWarning(exception, "OpenClaw wizard status failed");
            return GatewayFailure(exception, normalized);
        }
    }

    public async Task<OpenClawWizardResultDto> CancelAsync(
        string sessionId,
        OpenClawInvocationContext invocation,
        CancellationToken cancellationToken = default)
    {
        var normalized = NormalizeSessionId(sessionId);
        if (normalized is null || !sessions.ContainsKey(normalized))
        {
            return Failure(
                "not_found",
                "Die OpenClaw-Assistentensitzung ist nicht mehr verfügbar.",
                "Es wurde nichts abgebrochen.",
                normalized);
        }

        var unavailable = CheckAvailability("wizard.cancel");
        if (unavailable is not null)
            return unavailable with { SessionId = normalized };

        try
        {
            var response = await connector.InvokeAsync(
                "wizard.cancel",
                new JsonObject { ["sessionId"] = normalized },
                cancellationToken: cancellationToken,
                invocationContext: invocation with { IncludeIdempotencyParameter = false });
            sessions.TryRemove(normalized, out _);
            return new OpenClawWizardResultDto(
                true,
                "cancelled",
                "Der OpenClaw-Assistent wurde abgebrochen.",
                normalized,
                true,
                SafeString(response?["status"], 40) ?? "cancelled",
                SafeString(response?["error"], 1000),
                null,
                null,
                DateTimeOffset.UtcNow);
        }
        catch (Exception exception)
        {
            logger.LogWarning(exception, "OpenClaw wizard cancel failed");
            return GatewayFailure(exception, normalized);
        }
    }

    private OpenClawWizardResultDto MapResult(JsonNode? raw, string? sessionId)
    {
        var response = OpenClawPayloadSanitizer.Redact(raw) as JsonObject ?? new JsonObject();
        var resolvedSessionId = NormalizeSessionId(SafeString(response["sessionId"], 128))
            ?? NormalizeSessionId(sessionId);
        var done = ReadBool(response["done"]) ?? false;
        var status = SafeString(response["status"], 40) ?? (done ? "done" : "running");
        var error = SafeString(response["error"], 1000);
        var step = MapStep(response["step"]);

        if (!done && resolvedSessionId is not null && step is not null)
            sessions[resolvedSessionId] = new WizardSessionState(step.Id, step.Sensitive);
        else if (resolvedSessionId is not null)
            sessions.TryRemove(resolvedSessionId, out _);

        var ok = error is null && status != "error";
        return new OpenClawWizardResultDto(
            ok,
            done ? status : "waiting_for_input",
            done
                ? "Der offizielle OpenClaw-Assistent ist beendet."
                : step?.Sensitive == true
                    ? "OpenClaw fordert einen serverseitigen Secret-Schritt an."
                    : "OpenClaw wartet auf den nächsten Schritt.",
            resolvedSessionId,
            done,
            status,
            error,
            step,
            step?.Sensitive == true
                ? "Provider-Secrets ausschließlich in OpenClaw oder als serverseitigen SecretRef konfigurieren."
                : error,
            DateTimeOffset.UtcNow);
    }

    private static OpenClawWizardStepDto? MapStep(JsonNode? value)
    {
        if (value is not JsonObject step)
            return null;
        var id = SafeString(step["id"], 256);
        var type = SafeString(step["type"], 40);
        if (string.IsNullOrWhiteSpace(id) ||
            type is not ("note" or "select" or "text" or "confirm" or "multiselect" or "progress" or "action"))
        {
            return null;
        }

        var sensitive = ReadBool(step["sensitive"]) ?? false;
        var options = step["options"] is JsonArray optionArray
            ? optionArray
                .OfType<JsonObject>()
                .Select(option => new OpenClawWizardOptionDto(
                    option["value"]?.DeepClone(),
                    SafeString(option["label"], 500) ?? "Option",
                    SafeString(option["hint"], 1000)))
                .Take(200)
                .ToArray()
            : [];
        var externalUrl = SafeHttpUrl(SafeString(step["externalUrl"], 2048));
        var deviceCode = step["deviceCode"] is JsonObject device
            ? new OpenClawWizardDeviceCodeDto(
                SafeString(device["code"], 256) ?? string.Empty,
                ReadInt(device["expiresInMinutes"]),
                SafeString(device["message"], 1000))
            : null;

        return new OpenClawWizardStepDto(
            id,
            type,
            SafeString(step["title"], 500),
            SafeString(step["message"], 4000),
            options,
            sensitive ? null : step["initialValue"]?.DeepClone(),
            sensitive ? null : SafeString(step["placeholder"], 500),
            sensitive,
            SafeString(step["executor"], 40),
            externalUrl,
            deviceCode,
            !sensitive,
            sensitive
                ? "Nexus übernimmt keine Provider-Secrets aus dem Browser."
                : null);
    }

    private OpenClawWizardResultDto? CheckAvailability(string method)
    {
        if (managementState is not null && !managementState.Enabled)
            return Failure("management_disabled", "OpenClaw-Verwaltung ist in Nexus nicht freigegeben.", "Read-only-Adoption abschließen und Verwaltungsrechte bewusst aktivieren.");
        if (connector.ConnectionState != GatewayConnectionState.Connected)
            return Failure("disconnected", "OpenClaw Gateway ist nicht verbunden.", "Verbindung zuerst prüfen.");
        if (!connector.Supports(method))
            return Failure("unsupported", $"OpenClaw unterstützt {method} nicht.", "Gepinnte OpenClaw-Version prüfen.");
        if (!connector.GrantedScopes.Contains("operator.admin"))
            return Failure("scope_upgrade_required", "OpenClaw hat operator.admin nicht gewährt.", "Scope-Upgrade ausdrücklich pairen und erneut versuchen.");
        return null;
    }

    private static OpenClawWizardResultDto GatewayFailure(Exception exception, string? sessionId = null)
    {
        var state = exception is OpenClawGatewayRpcException gateway
            ? gateway.Code.ToLowerInvariant()
            : "gateway_error";
        return Failure(
            state,
            "Der offizielle OpenClaw-Assistent konnte nicht fortgesetzt werden.",
            "OpenClaw-Verbindung, Scope und Wizard-Status prüfen.",
            sessionId);
    }

    private static OpenClawWizardResultDto Failure(
        string state,
        string message,
        string recovery,
        string? sessionId = null)
        => new(
            false,
            state,
            message,
            sessionId,
            false,
            null,
            null,
            null,
            recovery,
            DateTimeOffset.UtcNow);

    private static string? NormalizeSessionId(string? value)
    {
        var normalized = value?.Trim();
        return string.IsNullOrWhiteSpace(normalized) ||
               normalized.Length > 128 ||
               normalized.Any(char.IsControl)
            ? null
            : normalized;
    }

    private static string? SafeString(JsonNode? value, int maxLength)
    {
        try
        {
            var text = value?.GetValue<string>()?.Trim();
            if (string.IsNullOrWhiteSpace(text))
                return null;
            return text.Length <= maxLength ? text : $"{text[..(maxLength - 1)]}…";
        }
        catch
        {
            return null;
        }
    }

    private static string? SafeHttpUrl(string? value)
        => Uri.TryCreate(value, UriKind.Absolute, out var uri) &&
           uri.Scheme is "https" or "http"
            ? uri.ToString()
            : null;

    private static bool? ReadBool(JsonNode? value)
    {
        try { return value?.GetValue<bool>(); }
        catch { return null; }
    }

    private static int? ReadInt(JsonNode? value)
    {
        try { return value?.GetValue<int>(); }
        catch { return null; }
    }

    private sealed record WizardSessionState(string StepId, bool Sensitive);
}
