// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Teams.Apps.Api.Clients;
using Microsoft.Teams.Apps.OAuth;
using Microsoft.Teams.Apps.Schema;
using Microsoft.Teams.Apps.Schema.Entities;
using Microsoft.Teams.Apps.State;
using Microsoft.Teams.Core;

namespace Microsoft.Teams.Apps;


/// <summary>
/// Context for a bot turn.
/// </summary>
/// <param name="botApplication">The bot application instance that owns this context.</param>
/// <param name="activity">The incoming activity for this turn.</param>
public class Context<TActivity>(TeamsBotApplication botApplication, TActivity activity) where TActivity : TeamsActivity
{
    /// <summary>
    /// Base bot application.
    /// </summary>
    public TeamsBotApplication TeamsBotApplication { get; } = botApplication;

    /// <summary>
    /// Current activity.
    /// </summary>
    public TActivity Activity { get; } = activity;

    /// <summary>
    /// Gets the application (client) ID configured for this bot.
    /// </summary>
    public string AppId => TeamsBotApplication.AppId;

    private ContextLogger? _log;

    /// <summary>
    /// Gets the logger for this context, providing <c>.Info()</c>, <c>.Error()</c>, <c>.Debug()</c>,
    /// and <c>.Warn()</c> convenience methods that delegate to the underlying <see cref="ILogger"/>.
    /// </summary>
    public ContextLogger Log => _log ??= new ContextLogger(TeamsBotApplication.Logger);

    private ApiClient? _api;

    /// <summary>
    /// Gets the <see cref="ApiClient"/> scoped to the current activity's service URL.
    /// </summary>
    public ApiClient Api => _api ??= TeamsBotApplication.Api.ForServiceUrl(
        Activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."));

    // ==================== Turn State ====================

    private TurnStateContainer? _state;

    /// <summary>
    /// Gets the per-turn state container with <see cref="TurnStateContainer.ConversationState"/>
    /// and <see cref="TurnStateContainer.UserState"/> scopes.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown when state management is not configured.</exception>
    public TurnStateContainer State
    {
        get => _state ?? throw new InvalidOperationException(
            "State is not available. Call UseState() during service registration, and if using a custom TeamsBotApplication make sure you pass a TurnStateLoader instance.");
        internal set => _state = value;
    }

    /// <summary>
    /// Returns true if state has been loaded for this turn.
    /// </summary>
    public bool HasState => _state is not null;

    /// <summary>
    /// Creates a copy of this context, preserving state if available.
    /// </summary>
    internal Context<TActivity> CreateDerivedContext()
    {
        Context<TActivity> derived = new(TeamsBotApplication, Activity);
        if (HasState)
        {
            derived.State = State;
        }
        return derived;
    }

    /// <summary>
    /// Creates a new context for a different activity type, preserving state if available.
    /// </summary>
    internal Context<TNew> CreateDerivedContext<TNew>(TNew activity) where TNew : TeamsActivity
    {
        Context<TNew> derived = new(TeamsBotApplication, activity);
        if (HasState)
        {
            derived.State = State;
        }
        return derived;
    }

    // ==================== Convenience Send/Reply/Typing ====================

    /// <summary>
    /// Sends a text message to the conversation.
    /// </summary>
    /// <param name="text">The text to send.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> SendAsync(string text, CancellationToken cancellationToken = default)
        => SendActivityAsync(text, cancellationToken);

    /// <summary>
    /// Sends an activity to the conversation.
    /// </summary>
    /// <param name="activity">The activity to send.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> SendAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
        => SendActivityAsync(activity, cancellationToken);

    /// <summary>
    /// Sends a text message as a threaded reply to the current activity. When the inbound activity
    /// has an id, the response auto-quotes it (rendered as a quote bubble above the response in Teams);
    /// otherwise sends without quoting.
    /// </summary>
    /// <param name="text">The text to send.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> ReplyAsync(string text, CancellationToken cancellationToken = default)
        => ReplyAsync(new MessageActivity(text), cancellationToken);

    /// <summary>
    /// Sends an activity to the conversation. When the inbound activity has an id, the response
    /// auto-quotes it (rendered as a quote bubble above the response in Teams). Otherwise sends
    /// without quoting. To send without quoting unconditionally, use <see cref="Send(TeamsActivity, CancellationToken)"/>.
    /// </summary>
    /// <param name="activity">The activity to send.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> ReplyAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(activity);
        if (!string.IsNullOrWhiteSpace(Activity.Id))
        {
            return Quote(Activity.Id, activity, cancellationToken);
        }

        return SendActivityAsync(activity, cancellationToken);
    }

    /// <summary>
    /// Sends a typing indicator to the conversation.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> TypingAsync(CancellationToken cancellationToken = default)
        => SendTypingActivityAsync(cancellationToken);

    /// <summary>
    /// Send a message to the conversation with a quoted message reference prepended to the text.
    /// Teams renders the quoted message as a preview bubble above the response text.
    /// </summary>
    /// <param name="messageId">The ID of the message to quote.</param>
    /// <param name="text">The response text, appended to the quoted message placeholder.</param>
    /// <param name="cancellationToken">Optional cancellation token.</param>
    /// <returns>The response from sending the activity.</returns>
    [Experimental("ExperimentalTeamsQuotedReplies")]
    public Task<SendActivityResponse?> Quote(string messageId, string text, CancellationToken cancellationToken = default)
        => Quote(messageId, new MessageActivity(text), cancellationToken);

    /// <summary>
    /// Send a message to the conversation with a quoted message reference prepended to the text.
    /// Teams renders the quoted message as a preview bubble above the response text.
    /// </summary>
    /// <param name="messageId">The ID of the message to quote.</param>
    /// <param name="activity">The activity to send. For <see cref="MessageActivity"/>, a quote placeholder for messageId is prepended to its text. Other activity types are sent as-is without quoting.</param>
    /// <param name="cancellationToken">Optional cancellation token.</param>
    /// <returns>The response from sending the activity.</returns>
    [Experimental("ExperimentalTeamsQuotedReplies")]
    public Task<SendActivityResponse?> Quote(string messageId, TeamsActivity activity, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(activity);
        ArgumentException.ThrowIfNullOrWhiteSpace(messageId);
        if (activity is MessageActivity message)
        {
            message.PrependQuote(messageId);
        }
        return SendActivityAsync(activity, cancellationToken);
    }

    /// <inheritdoc cref="SendAsync(string, CancellationToken)"/>
    public Task<SendActivityResponse?> Send(string text, CancellationToken cancellationToken = default)
        => SendAsync(text, cancellationToken);

    /// <inheritdoc cref="SendAsync(TeamsActivity, CancellationToken)"/>
    public Task<SendActivityResponse?> Send(TeamsActivity activity, CancellationToken cancellationToken = default)
        => SendAsync(activity, cancellationToken);

    /// <inheritdoc cref="ReplyAsync(string, CancellationToken)"/>
    public Task<SendActivityResponse?> Reply(string text, CancellationToken cancellationToken = default)
        => ReplyAsync(text, cancellationToken);

    /// <inheritdoc cref="ReplyAsync(TeamsActivity, CancellationToken)"/>
    public Task<SendActivityResponse?> Reply(TeamsActivity activity, CancellationToken cancellationToken = default)
        => ReplyAsync(activity, cancellationToken);

    /// <inheritdoc cref="TypingAsync(CancellationToken)"/>
    public Task<SendActivityResponse?> Typing(CancellationToken cancellationToken = default)
        => TypingAsync(cancellationToken);

    // ==================== Core Send Methods ====================

    /// <summary>
    /// Sends a message activity as a reply.
    /// </summary>
    /// <param name="text">The text to send.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> SendActivityAsync(string text, CancellationToken cancellationToken = default)
        => SendActivityAsync(new MessageActivity(text) { TextFormat = TextFormats.Plain }, cancellationToken);

    /// <summary>
    /// Sends an activity to the conversation.
    /// </summary>
    /// <param name="activity">The activity to send.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> SendActivityAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(activity);

        bool isTargeted = activity.Recipient?.IsTargeted == true;

        if (isTargeted && Activity.Conversation?.ConversationType == ConversationType.Personal)
        {
            throw new InvalidOperationException(
                "Targeted messages are not supported in personal (1:1) chats.");
        }

        if (activity.Type == TeamsActivityType.Message
            && Activity.Recipient?.IsTargeted == true
            && Activity.Id is not null)
        {
            TargetedMessageInfoEntityExtensions.AddToActivity(activity, Activity.Id);
        }

        TeamsActivity reply = new TeamsActivityBuilder(activity)
            .WithConversationReference(Activity)
            .Build();
        return TeamsBotApplication.SendActivityAsync(reply, cancellationToken: cancellationToken);
    }

    /// <summary>
    /// Sends a typing activity to the conversation asynchronously.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The response from the send operation.</returns>
    public Task<SendActivityResponse?> SendTypingActivityAsync(CancellationToken cancellationToken = default)
    {
        TeamsActivity reply = new TeamsActivityBuilder()
            .WithType(TeamsActivityType.Typing)
            .WithConversationReference(Activity)
            .Build();
        return TeamsBotApplication.SendActivityAsync(reply, cancellationToken: cancellationToken);
    }

    // ==================== OAuth Sign-In ====================

    /// <summary>
    /// Trigger user OAuth sign-in flow for the activity sender.
    /// Attempts silent token acquisition first; if no token is cached, sends an OAuthCard.
    /// </summary>
    /// <param name="options">OAuth options including connection name and card text.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The existing user token if found, or null if the sign-in flow was initiated.</returns>
    public Task<string?> SignInAsync(OAuthOptions? options = null, CancellationToken cancellationToken = default)
    {
        OAuthFlow flow = ResolveOAuthFlow(options?.ConnectionName);
        return flow.SignInAsync(this, options, cancellationToken);
    }

    /// <summary>
    /// Sign the user out, revoking their token from the Bot Framework Token Store.
    /// </summary>
    /// <param name="connectionName">The connection name to sign out from. If null, uses the default registered connection.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    public Task SignOutAsync(string? connectionName = null, CancellationToken cancellationToken = default)
    {
        OAuthFlow flow = ResolveOAuthFlow(connectionName);
        return flow.SignOutAsync(this, cancellationToken);
    }

    /// <inheritdoc cref="SignInAsync(OAuthOptions?, CancellationToken)"/>
    public Task<string?> SignIn(OAuthOptions? options = null, CancellationToken cancellationToken = default)
        => SignInAsync(options, cancellationToken);

    /// <inheritdoc cref="SignOutAsync(string?, CancellationToken)"/>
    public Task SignOut(string? connectionName = null, CancellationToken cancellationToken = default)
        => SignOutAsync(connectionName, cancellationToken);

    /// <summary>
    /// Whether the activity sender has a valid cached token.
    /// </summary>
    /// <remarks>
    /// This property is no longer supported because it used sync-over-async
    /// (<c>.GetAwaiter().GetResult()</c>) to call the remote Token Service,
    /// which causes thread-pool starvation under load.
    /// Use <see cref="IsSignedInAsync"/> instead.
    /// </remarks>
    [Obsolete("Use IsSignedInAsync() instead. This property always throws NotSupportedException.")]
    public bool IsSignedIn
        => throw new NotSupportedException(
            "IsSignedIn is no longer supported because it blocks the calling thread. Use 'await IsSignedInAsync()' instead.");

    /// <summary>
    /// Check whether the user has a valid cached token for a given OAuth connection.
    /// </summary>
    /// <param name="connectionName">The connection name to check. If null, uses the single registered connection.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>True if the user has a valid token; false otherwise.</returns>
    public Task<bool> IsSignedInAsync(string? connectionName = null, CancellationToken cancellationToken = default)
    {
        OAuthFlow flow = ResolveOAuthFlow(connectionName);
        return flow.IsSignedInAsync(this, cancellationToken);
    }

    /// <summary>
    /// Get the token status for all configured OAuth connections.
    /// Returns every connection registered on the bot, so the developer
    /// never needs to enumerate connection names manually.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>A list of token status results for all configured connections.</returns>
    public Task<IList<GetTokenStatusResult>> GetConnectionStatusAsync(CancellationToken cancellationToken = default)
    {
        OAuthFlowRegistry registry = TeamsBotApplication.OAuthRegistry
            ?? throw new InvalidOperationException("No OAuthFlow registered. Call AddOAuthFlow(connectionName) on the TeamsBotApplication first.");

        // Use any flow -- GetConnectionStatusAsync returns all connections regardless
        OAuthFlow flow = registry.ResolveSingle()
            ?? registry.GetAllFlows().First();

        return flow.GetConnectionStatusAsync(this, cancellationToken);
    }

    private OAuthFlow ResolveOAuthFlow(string? connectionName)
    {
        OAuthFlowRegistry registry = TeamsBotApplication.OAuthRegistry
            ?? throw new InvalidOperationException("No OAuthFlow registered. Call AddOAuthFlow(connectionName) on the TeamsBotApplication first.");

        if (connectionName is not null)
        {
            OAuthFlow? flow = registry.Resolve(connectionName);
            if (flow is not null) return flow;

            string registered = string.Join(", ", registry.GetRegisteredConnectionNames().Select(n => $"'{n}'"));
            throw new InvalidOperationException(
                $"No OAuthFlow registered for connection '{connectionName}'. " +
                $"Registered connections: {(registered.Length > 0 ? registered : "(none)")}.");
        }

        return registry.ResolveSingle()
            ?? throw new InvalidOperationException(
                "Multiple OAuthFlow instances registered. Specify a connection name in OAuthOptions or SignOut(connectionName).");
    }
}
