// Copyright (c) Microsoft. All rights reserved.

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;

[SendsMessage(typeof(ExternalInputRequest))]
[SendsMessage(typeof(ExternalInputResponse))]
internal sealed class QuestionExecutor(Question model, ResponseAgentProvider agentProvider, WorkflowFormulaState state) :
    DeclarativeActionExecutor<Question>(model, state)
{
    public static class Steps
    {
        public static string Prepare(string id) => $"{id}_{nameof(Prepare)}";
        public static string Input(string id) => $"{id}_{nameof(Input)}";
        public static string Capture(string id) => $"{id}_{nameof(Capture)}";
    }

    private readonly DurableProperty<int> _promptCount = new(nameof(_promptCount));
    private readonly DurableProperty<bool> _hasExecuted = new(nameof(_hasExecuted));

    protected override bool IsDiscreteAction => false;
    protected override bool EmitResultEvent => false;

    // Input has been captured when Result is null
    public static bool IsComplete(object? message)
    {
        ActionExecutorResult executorMessage = ActionExecutorResult.ThrowIfNot(message);
        return executorMessage.Result is null;
    }

    protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
    {
        InitializablePropertyPath variable = Throw.IfNull(this.Model.Variable);
        bool isValueUndefined = context.ReadState(variable.Path) is BlankValue;
        // Snapshot prior-execution state before we mutate it below so the SkipQuestionMode
        // evaluation reflects whether this is the first time the action has run.
        bool hasExecutedPreviously = await this._hasExecuted.ReadAsync(context).ConfigureAwait(false);
        bool proceed = this.Evaluator.GetValue(this.Model.AlwaysPrompt).Value;

        if (!proceed)
        {
            SkipQuestionMode mode = this.Evaluator.GetValue(this.Model.SkipQuestionMode).Value;
            proceed =
                mode switch
                {
                    SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue => isValueUndefined || hasExecutedPreviously,
                    SkipQuestionMode.AlwaysSkipIfVariableHasValue => isValueUndefined,
                    SkipQuestionMode.AlwaysAsk => true,
                    _ => true,
                };
        }

        // Record that the action has executed in the same executor scope as the read above.
        // (CaptureResponseAsync runs in a different executor's state scope, so writing it there
        // would not be visible to subsequent ExecuteAsync invocations triggered by GotoAction.)
        await this._hasExecuted.WriteAsync(context, true).ConfigureAwait(false);

        if (proceed)
        {
            // Initial prompt: count is 0 because no responses have been received yet for this turn.
            // _promptCount itself is tracked in CaptureResponseAsync's scope (see comment on _promptCount).
            await this.PromptAsync(context, actualCount: 0, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
        }

        return default;
    }

    public async ValueTask PrepareResponseAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
    {
        ExternalInputRequest inputRequest = new(this.FormatPrompt(this.Model.Prompt));
        await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
    }

    public async ValueTask CaptureResponseAsync(IWorkflowContext context, ExternalInputResponse response, CancellationToken cancellationToken)
    {
        // _promptCount is tracked in this (Capture) executor's scope so reads and writes are coherent.
        // Each Capture invocation represents an attempt to satisfy the question; increment up front
        // and pass the value to PromptAsync explicitly so the retry/default decision is scope-independent.
        int promptCount = await this._promptCount.ReadAsync(context).ConfigureAwait(false) + 1;
        await this._promptCount.WriteAsync(context, promptCount).ConfigureAwait(false);

        FormulaValue? extractedValue = null;
        if (!response.HasMessages)
        {
            string unrecognizedResponse = this.Model.UnrecognizedPrompt is not null ? this.FormatPrompt(this.Model.UnrecognizedPrompt) : "Invalid response";
            await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false);
        }
        else
        {
            EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, string.Concat(response.Messages.Select(message => message.Text)));
            if (entityResult.IsValid)
            {
                extractedValue = entityResult.Value;
            }
            else
            {
                string invalidResponse = this.Model.InvalidPrompt is not null ? this.FormatPrompt(this.Model.InvalidPrompt) : "Invalid response";
                await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim()), cancellationToken).ConfigureAwait(false);
            }
        }

        if (extractedValue is null)
        {
            await this.PromptAsync(context, promptCount, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            // Reset for any subsequent Question turn (e.g. via GotoAction re-entry) so the next attempt starts fresh.
            await this._promptCount.WriteAsync(context, 0).ConfigureAwait(false);
            bool autoSend = true;

            if (this.Model.ExtensionData?.Properties.TryGetValue("autoSend", out DataValue? autoSendValue) ?? false)
            {
                autoSend = autoSendValue.ToObject() is bool value && value;
            }

            if (autoSend)
            {
                string? workflowConversationId = context.GetWorkflowConversation();
                if (workflowConversationId is not null)
                {
                    // Input message expected to be defined when values have been extracted, but guard defensively.
                    ChatMessage? input = response.Messages.LastOrDefault();
                    if (input is not null)
                    {
                        await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false);
                        await context.SetLastMessageAsync(input).ConfigureAwait(false);
                    }
                }
            }

            await this.AssignAsync(Throw.IfNull(this.Model.Variable).Path, extractedValue, context).ConfigureAwait(false);
            await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
        }
    }

    public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
    {
        await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
    }

    private async ValueTask PromptAsync(IWorkflowContext context, int actualCount, CancellationToken cancellationToken)
    {
        long repeatCount = this.Evaluator.GetValue(this.Model.RepeatCount).Value;
        if (actualCount >= repeatCount)
        {
            DataValue defaultValue = DataValue.Blank();
            if (this.Model.DefaultValue is not null)
            {
                ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue);
                defaultValue = this.Evaluator.GetValue(defaultValueExpression).Value;
            }
            await this.AssignAsync(Throw.IfNull(this.Model.Variable).Path, defaultValue.ToFormula(), context).ConfigureAwait(false);
            string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse);
            await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim()), cancellationToken).ConfigureAwait(false);
            // Reset for any subsequent Question turn (e.g. via GotoAction re-entry) so the next attempt starts fresh.
            await this._promptCount.WriteAsync(context, 0).ConfigureAwait(false);
            await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            await context.SendResultMessageAsync(this.Id, result: true, cancellationToken).ConfigureAwait(false);
        }
    }

    private string FormatPrompt(ActivityTemplateBase? promptTemplate)
    {
        if (promptTemplate is not MessageActivityTemplate messageActivity)
        {
            return string.Empty;
        }

        return this.Engine.Format(messageActivity.Text).Trim();
    }
}
