using System.Text.Json.Nodes;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Nexus.Api.Repositories;

namespace Nexus.Api.Services;

public sealed class OpenClawRunService(
    IOpenClawRunRepository runs,
    IOpenClawRunGateway gateway) : IOpenClawRunService
{
    private const string ResumeCapabilityMessage =
        "OpenClaw exposes chat.send/chat.abort/history, but no durable same-run resume RPC. "
        + "Use Retry to create a correlated new run or start a follow-up turn.";

    public async Task<OpenClawRunCollectionDto> GetAsync(
        OpenClawRunQuery query,
        CancellationToken cancellationToken = default)
    {
        var limit = Math.Clamp(query.Limit, 1, 200);
        var items = await runs.GetAsync(query with { Limit = limit + 1 }, cancellationToken);
        var hasMore = items.Count > limit;
        var selected = items.Take(limit).ToList();
        var nextCursor = hasMore && selected.Count > 0
            ? OpenClawRunCursorCodec.Encode(
                selected[^1].CreatedAt,
                selected[^1].Id)
            : null;

        return new OpenClawRunCollectionDto(
            selected.Select(Map).ToList(),
            nextCursor,
            DateTimeOffset.UtcNow);
    }

    public async Task<OpenClawRunDto?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        var run = await runs.GetByIdAsync(id, cancellationToken: cancellationToken);
        return run is null ? null : Map(run);
    }

    public async Task<OpenClawRunOperationDto> StartAsync(
        StartOpenClawRunRequest request,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
    {
        var existing = await runs.GetByStartIdempotencyKeyAsync(
            invocation.IdempotencyKey,
            cancellationToken);
        if (existing is not null)
        {
            var sameCommand = string.Equals(existing.Prompt, request.Prompt.Trim(), StringComparison.Ordinal)
                              && string.Equals(existing.AgentId, request.AgentId.Trim(), StringComparison.Ordinal)
                              && string.Equals(existing.SessionKey, request.SessionKey.Trim(), StringComparison.Ordinal);
            if (sameCommand
                && existing.Status == OpenClawRunStates.Dispatching
                && string.IsNullOrWhiteSpace(existing.OpenClawRunId))
            {
                var recoverable = await runs.GetByIdAsync(
                    existing.Id,
                    tracking: true,
                    cancellationToken: cancellationToken);
                if (recoverable is not null)
                {
                    var recovered = await gateway.StartAsync(recoverable, invocation, cancellationToken);
                    ApplyDispatchResult(recoverable, recovered);
                    await runs.UpdateAsync(
                        recoverable,
                        History(
                            recoverable,
                            "start_recovery",
                            OpenClawRunStates.Dispatching,
                            recoverable.Status,
                            $"Idempotent dispatch recovery: {recovered.Message}",
                            invocation,
                            idempotencyKey: null),
                        cancellationToken);
                    return Operation(
                        recovered.Ok,
                        recovered.State,
                        recovered.Message,
                        recoverable);
                }
            }

            return Operation(
                sameCommand,
                sameCommand ? "idempotent_replay" : "idempotency_conflict",
                sameCommand
                    ? "The original start result was returned; OpenClaw was not invoked again."
                    : "The idempotency key is already bound to a different run command.",
                existing);
        }

        await ValidateCorrelationsAsync(request.TaskId, request.ProjectId, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var run = new OpenClawRun
        {
            Title = BuildTitle(request.Title, request.Prompt),
            Prompt = request.Prompt.Trim(),
            AgentId = request.AgentId.Trim(),
            SessionKey = request.SessionKey.Trim(),
            TaskId = request.TaskId,
            ProjectId = request.ProjectId,
            Status = OpenClawRunStates.Dispatching,
            StartIdempotencyKey = invocation.IdempotencyKey,
            CorrelationId = invocation.CorrelationId,
            Actor = invocation.Actor,
            TraceParent = invocation.TraceParent,
            CreatedAt = now,
            UpdatedAt = now
        };
        await runs.AddAsync(
            run,
            History(
                run,
                "start_requested",
                OpenClawRunStates.Dispatching,
                OpenClawRunStates.Dispatching,
                "Nexus durably recorded the run before dispatch.",
                invocation),
            cancellationToken);

        var result = await gateway.StartAsync(run, invocation, cancellationToken);
        ApplyDispatchResult(run, result);
        await runs.UpdateAsync(
            run,
            History(
                run,
                "start_result",
                OpenClawRunStates.Dispatching,
                run.Status,
                result.Message,
                invocation,
                idempotencyKey: null),
            cancellationToken);

        return Operation(result.Ok, result.State, result.Message, run);
    }

    public async Task<OpenClawRunOperationDto?> StopAsync(
        Guid id,
        string? reason,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
    {
        var run = await runs.GetByIdAsync(id, tracking: true, cancellationToken: cancellationToken);
        if (run is null)
            return null;

        var replay = await runs.GetInvocationAsync(
            id,
            "stop_requested",
            invocation.IdempotencyKey,
            cancellationToken);
        if (replay is not null)
        {
            return Operation(
                true,
                "idempotent_replay",
                "The original stop result was returned; OpenClaw was not invoked again.",
                run);
        }

        if (OpenClawRunStates.IsTerminal(run.Status))
        {
            var message = $"Run is already terminal with state '{run.Status}'.";
            await runs.UpdateAsync(
                run,
                History(
                    run,
                    "stop_requested",
                    run.Status,
                    run.Status,
                    message,
                    invocation),
                cancellationToken);
            return Operation(true, "already_terminal", message, run);
        }

        var previousState = run.Status;
        run.Status = OpenClawRunStates.Stopping;
        await runs.UpdateAsync(
            run,
            History(
                run,
                "stop_requested",
                previousState,
                OpenClawRunStates.Stopping,
                BuildReasonMessage("Stop requested.", reason),
                invocation),
            cancellationToken);

        var result = await gateway.StopAsync(run, invocation, cancellationToken);
        run.Status = result.Ok ? OpenClawRunStates.Stopped : previousState;
        run.LastError = result.Ok ? null : result.Message;
        if (result.Ok)
            run.FinishedAt = DateTimeOffset.UtcNow;

        await runs.UpdateAsync(
            run,
            History(
                run,
                "stop_result",
                OpenClawRunStates.Stopping,
                run.Status,
                result.Message,
                invocation,
                idempotencyKey: null),
            cancellationToken);
        return Operation(result.Ok, result.State, result.Message, run);
    }

    public async Task<OpenClawRunOperationDto?> ResumeAsync(
        Guid id,
        string? reason,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
    {
        var run = await runs.GetByIdAsync(id, tracking: true, cancellationToken: cancellationToken);
        if (run is null)
            return null;

        var replay = await runs.GetInvocationAsync(
            id,
            "resume_requested",
            invocation.IdempotencyKey,
            cancellationToken);
        if (replay is not null)
        {
            return Operation(
                false,
                OpenClawRunStates.Unsupported,
                ResumeCapabilityMessage,
                run);
        }

        await runs.UpdateAsync(
            run,
            History(
                run,
                "resume_requested",
                run.Status,
                run.Status,
                BuildReasonMessage(ResumeCapabilityMessage, reason),
                invocation),
            cancellationToken);

        return Operation(
            false,
            OpenClawRunStates.Unsupported,
            ResumeCapabilityMessage,
            run);
    }

    public async Task<OpenClawRunOperationDto?> RetryAsync(
        Guid id,
        string? reason,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
    {
        var source = await runs.GetByIdAsync(id, tracking: true, cancellationToken: cancellationToken);
        if (source is null)
            return null;

        var existing = await runs.GetByStartIdempotencyKeyAsync(
            invocation.IdempotencyKey,
            cancellationToken);
        if (existing is not null)
        {
            var sameSource = existing.RetriedFromRunId == id;
            if (sameSource
                && existing.Status == OpenClawRunStates.Dispatching
                && string.IsNullOrWhiteSpace(existing.OpenClawRunId))
            {
                var recoverable = await runs.GetByIdAsync(
                    existing.Id,
                    tracking: true,
                    cancellationToken: cancellationToken);
                if (recoverable is not null)
                {
                    var recovered = await gateway.StartAsync(recoverable, invocation, cancellationToken);
                    ApplyDispatchResult(recoverable, recovered);
                    await runs.UpdateAsync(
                        recoverable,
                        History(
                            recoverable,
                            "start_recovery",
                            OpenClawRunStates.Dispatching,
                            recoverable.Status,
                            $"Idempotent retry dispatch recovery: {recovered.Message}",
                            invocation,
                            idempotencyKey: null),
                        cancellationToken);
                    return Operation(
                        recovered.Ok,
                        recovered.State,
                        recovered.Message,
                        source,
                        recoverable);
                }
            }

            return Operation(
                sameSource,
                sameSource ? "idempotent_replay" : "idempotency_conflict",
                sameSource
                    ? "The original retry result was returned; OpenClaw was not invoked again."
                    : "The idempotency key is already bound to another run.",
                source,
                existing);
        }

        var rejectedReplay = await runs.GetInvocationAsync(
            id,
            "retry_rejected",
            invocation.IdempotencyKey,
            cancellationToken);
        if (rejectedReplay is not null)
        {
            return Operation(
                false,
                "invalid_state",
                "Only a terminal, blocked, or unsupported run can be retried.",
                source);
        }

        if (!OpenClawRunStates.IsTerminal(source.Status))
        {
            const string message = "Only a terminal, blocked, or unsupported run can be retried.";
            await runs.UpdateAsync(
                source,
                History(
                    source,
                    "retry_rejected",
                    source.Status,
                    source.Status,
                    BuildReasonMessage(message, reason),
                    invocation),
                cancellationToken);
            return Operation(false, "invalid_state", message, source);
        }

        var now = DateTimeOffset.UtcNow;
        var retry = new OpenClawRun
        {
            Title = source.Title,
            Prompt = source.Prompt,
            AgentId = source.AgentId,
            SessionKey = source.SessionKey,
            TaskId = source.TaskId,
            ProjectId = source.ProjectId,
            RetriedFromRunId = source.Id,
            Status = OpenClawRunStates.Dispatching,
            StartIdempotencyKey = invocation.IdempotencyKey,
            CorrelationId = invocation.CorrelationId,
            Actor = invocation.Actor,
            TraceParent = invocation.TraceParent,
            CreatedAt = now,
            UpdatedAt = now
        };

        await runs.AddRetryAsync(
            source,
            retry,
            History(
                source,
                "retry_requested",
                source.Status,
                source.Status,
                BuildReasonMessage($"Retry created run {retry.Id}.", reason),
                invocation,
                resultRunId: retry.Id),
            History(
                retry,
                "start_requested",
                OpenClawRunStates.Dispatching,
                OpenClawRunStates.Dispatching,
                $"Retry of Nexus run {source.Id} was durably recorded before dispatch.",
                invocation),
            cancellationToken);

        var result = await gateway.StartAsync(retry, invocation, cancellationToken);
        ApplyDispatchResult(retry, result);
        await runs.UpdateAsync(
            retry,
            History(
                retry,
                "start_result",
                OpenClawRunStates.Dispatching,
                retry.Status,
                result.Message,
                invocation,
                idempotencyKey: null),
            cancellationToken);

        return Operation(result.Ok, result.State, result.Message, source, retry);
    }

    public async Task<OpenClawRunHistoryResponse?> GetHistoryAsync(
        Guid id,
        int gatewayLimit = 200,
        CancellationToken cancellationToken = default)
    {
        var run = await runs.GetByIdAsync(id, cancellationToken: cancellationToken);
        if (run is null)
            return null;

        var transitions = await runs.GetHistoryAsync(id, cancellationToken);
        var gatewayHistory = await gateway.GetHistoryAsync(
            run,
            Math.Clamp(gatewayLimit, 1, 1000),
            cancellationToken);

        return new OpenClawRunHistoryResponse(
            Map(run),
            transitions.Select(Map).ToList(),
            gatewayHistory.State,
            gatewayHistory.Data,
            gatewayHistory.Message,
            DateTimeOffset.UtcNow);
    }

    public async Task ReconcileAsync(
        GatewayEventEnvelope gatewayEvent,
        CancellationToken cancellationToken = default)
    {
        if (!IsRunEvent(gatewayEvent.Event))
            return;

        var runId = ReadString(gatewayEvent.Payload?["runId"]);
        var state = ReadString(gatewayEvent.Payload?["state"])?.ToLowerInvariant();
        if (string.IsNullOrWhiteSpace(runId) || string.IsNullOrWhiteSpace(state))
            return;

        var projectedState = MapGatewayState(state);
        if (projectedState is null)
            return;

        var gatewayEventId = OpenClawEventIdentity.Create(gatewayEvent);
        if (await runs.HasGatewayEventAsync(gatewayEventId, cancellationToken))
            return;

        var run = await runs.GetByOpenClawRunIdAsync(runId, cancellationToken);
        if (run is null)
            return;

        var sequence = ReadLong(gatewayEvent.Payload?["seq"]);
        if (sequence.HasValue
            && run.LastGatewaySequence.HasValue
            && sequence.Value <= run.LastGatewaySequence.Value)
        {
            return;
        }

        var previousStatus = run.Status;
        var sequenceGap = sequence.HasValue
                          && run.LastGatewaySequence.HasValue
                          && sequence.Value > run.LastGatewaySequence.Value + 1;
        if (!OpenClawRunStates.IsTerminal(run.Status) || OpenClawRunStates.IsTerminal(projectedState))
            run.Status = projectedState;
        if (sequence.HasValue)
            run.LastGatewaySequence = sequence;
        run.SequenceGapDetected |= sequenceGap;
        if (run.StartedAt is null && projectedState == OpenClawRunStates.Running)
            run.StartedAt = gatewayEvent.ReceivedAt;
        if (OpenClawRunStates.IsTerminal(projectedState))
            run.FinishedAt = gatewayEvent.ReceivedAt;
        if (projectedState == OpenClawRunStates.Failed)
            run.LastError = ReadString(gatewayEvent.Payload?["errorMessage"]) ?? "OpenClaw run failed.";
        else if (projectedState is OpenClawRunStates.Completed or OpenClawRunStates.Stopped)
            run.LastError = null;

        var shouldAudit = !string.Equals(previousStatus, run.Status, StringComparison.Ordinal)
                          || sequenceGap
                          || OpenClawRunStates.IsTerminal(projectedState);
        OpenClawRunHistory? history = null;
        if (shouldAudit)
        {
            history = new OpenClawRunHistory
            {
                RunId = run.Id,
                Action = "gateway_event",
                FromStatus = previousStatus,
                ToStatus = run.Status,
                Message = sequenceGap
                    ? $"Gateway event '{gatewayEvent.Event}' reported a per-run sequence gap."
                    : $"Gateway event '{gatewayEvent.Event}' projected run state '{state}'.",
                Actor = "openclaw-gateway",
                CorrelationId = run.CorrelationId,
                TraceParent = run.TraceParent,
                GatewayEventId = gatewayEventId,
                GatewaySequence = sequence,
                SequenceGapDetected = sequenceGap,
                OccurredAt = gatewayEvent.ReceivedAt
            };
        }

        await runs.UpdateProjectionAsync(run, history, cancellationToken);
    }

    private async Task ValidateCorrelationsAsync(
        Guid? taskId,
        Guid? projectId,
        CancellationToken cancellationToken)
    {
        if (taskId.HasValue && !await runs.TaskExistsAsync(taskId.Value, cancellationToken))
            throw new OpenClawRunValidationException("taskId", "The correlated Nexus task does not exist.");
        if (projectId.HasValue && !await runs.ProjectExistsAsync(projectId.Value, cancellationToken))
            throw new OpenClawRunValidationException("projectId", "The correlated Nexus project does not exist.");
    }

    private static void ApplyDispatchResult(OpenClawRun run, OpenClawRunGatewayResult result)
    {
        run.Status = result.State;
        run.OpenClawRunId = result.OpenClawRunId ?? run.OpenClawRunId;
        run.LastError = result.Ok ? null : result.Message;
        if (result.Ok && run.StartedAt is null)
            run.StartedAt = DateTimeOffset.UtcNow;
        if (result.Ok && result.State == OpenClawRunStates.Completed)
            run.FinishedAt = DateTimeOffset.UtcNow;
    }

    private static OpenClawRunHistory History(
        OpenClawRun run,
        string action,
        string fromStatus,
        string toStatus,
        string message,
        OpenClawInvocationMetadata invocation,
        string? idempotencyKey = "__invocation__",
        Guid? resultRunId = null)
        => new()
        {
            RunId = run.Id,
            Action = action,
            FromStatus = fromStatus,
            ToStatus = toStatus,
            Message = message,
            Actor = invocation.Actor,
            CorrelationId = invocation.CorrelationId,
            IdempotencyKey = idempotencyKey == "__invocation__"
                ? invocation.IdempotencyKey
                : idempotencyKey,
            TraceParent = invocation.TraceParent,
            ResultRunId = resultRunId
        };

    private static OpenClawRunOperationDto Operation(
        bool ok,
        string state,
        string message,
        OpenClawRun run,
        OpenClawRun? resultRun = null)
        => new(
            ok,
            state,
            message,
            Map(run),
            resultRun is null ? null : Map(resultRun),
            DateTimeOffset.UtcNow,
            new OperationResultDto(
                run.CorrelationId,
                state,
                run.Revision,
                new EntityRefDto("run", run.Id.ToString(), run.Title),
                resultRun is null
                    ? []
                    : [new EntityRefDto(
                        "run",
                        resultRun.Id.ToString(),
                        resultRun.Title)],
                run.TraceParent));

    private static OpenClawRunDto Map(OpenClawRun run)
        => new(
            run.Id,
            run.Title,
            run.Prompt,
            run.AgentId,
            run.SessionKey,
            run.Status,
            run.TaskId,
            run.ProjectId,
            run.OpenClawRunId,
            run.RetriedFromRunId,
            run.CorrelationId,
            run.Actor,
            run.LastError,
            run.LastGatewaySequence,
            run.SequenceGapDetected,
            run.Status is OpenClawRunStates.Dispatching or OpenClawRunStates.Running or OpenClawRunStates.Stopping,
            OpenClawRunStates.IsTerminal(run.Status),
            false,
            ResumeCapabilityMessage,
            run.CreatedAt,
            run.UpdatedAt,
            run.StartedAt,
            run.FinishedAt);

    private static OpenClawRunHistoryDto Map(OpenClawRunHistory item)
        => new(
            item.Id,
            item.RunId,
            item.Action,
            item.FromStatus,
            item.ToStatus,
            item.Message,
            item.Actor,
            item.CorrelationId,
            item.IdempotencyKey,
            item.TraceParent,
            item.GatewayEventId,
            item.GatewaySequence,
            item.SequenceGapDetected,
            item.ResultRunId,
            item.OccurredAt);

    private static string BuildTitle(string? requestedTitle, string prompt)
    {
        var title = string.IsNullOrWhiteSpace(requestedTitle)
            ? prompt.Replace("\r", " ", StringComparison.Ordinal)
                .Replace("\n", " ", StringComparison.Ordinal)
                .Trim()
            : requestedTitle.Trim();
        return title.Length <= 160 ? title : title[..157] + "...";
    }

    private static string BuildReasonMessage(string message, string? reason)
        => string.IsNullOrWhiteSpace(reason)
            ? message
            : $"{message} Reason: {reason.Trim()}";

    private static bool IsRunEvent(string eventName)
        => eventName.Equals("chat", StringComparison.OrdinalIgnoreCase)
           || eventName.Equals("agent", StringComparison.OrdinalIgnoreCase)
           || eventName.Equals("session.operation", StringComparison.OrdinalIgnoreCase);

    private static string? MapGatewayState(string state)
        => state switch
        {
            "status" or "delta" or "started" or "running" => OpenClawRunStates.Running,
            "final" or "completed" or "succeeded" => OpenClawRunStates.Completed,
            "aborted" or "cancelled" => OpenClawRunStates.Stopped,
            "error" or "failed" => OpenClawRunStates.Failed,
            _ => null
        };

    private static string? ReadString(JsonNode? node)
    {
        try
        {
            return node?.GetValue<string>();
        }
        catch
        {
            return null;
        }
    }

    private static long? ReadLong(JsonNode? node)
    {
        try
        {
            if (node is null)
                return null;
            return node.GetValueKind() switch
            {
                System.Text.Json.JsonValueKind.Number => node.GetValue<long>(),
                System.Text.Json.JsonValueKind.String when long.TryParse(node.GetValue<string>(), out var value) => value,
                _ => null
            };
        }
        catch
        {
            return null;
        }
    }
}

public sealed class OpenClawRunValidationException(string field, string message) : Exception(message)
{
    public string Field { get; } = field;
}
