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

using System.Text.Json;
using System.Text.Json.Serialization;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Teams.Api.Activities;
using Microsoft.Teams.Api.Auth;
using Microsoft.Teams.Api.Clients;
using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.Events;
using Microsoft.Teams.Apps.Plugins;
using Microsoft.Teams.Common.Http;
using Microsoft.Teams.Common.Logging;

using HttpRequest = Microsoft.AspNetCore.Http.HttpRequest;

namespace Microsoft.Teams.Plugins.AspNetCore;

[Plugin]
public partial class AspNetCorePlugin : ISenderPlugin, IAspNetCorePlugin
{
    [Dependency]
    public ILogger Logger { get; set; }

    [Dependency("Token", optional: true)]
    public IToken? Token { get; set; }

    [Dependency]
    public IHttpClient Client { get; set; }

    public event EventFunction Events;

    private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public IApplicationBuilder Configure(IApplicationBuilder builder)
    {
        return builder;
    }

    public Task OnInit(App app, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public Task OnStart(App app, CancellationToken cancellationToken = default)
    {
        Logger.Debug("OnStart");
        return Task.CompletedTask;
    }

    public Task OnError(App app, IPlugin plugin, ErrorEvent @event, CancellationToken cancellationToken = default)
    {
        Logger.Debug("OnError");
        return Task.CompletedTask;
    }

    public Task OnActivity(App app, ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default)
    {
        Logger.Debug("OnActivity");
        return Task.CompletedTask;
    }

    public Task OnActivitySent(App app, ISenderPlugin sender, ActivitySentEvent @event, CancellationToken cancellationToken = default)
    {
        Logger.Debug("OnActivitySent");
        return Task.CompletedTask;
    }

    public Task OnActivityResponse(App app, ISenderPlugin sender, ActivityResponseEvent @event, CancellationToken cancellationToken = default)
    {
        Logger.Debug("OnActivityResponse");
        return Task.CompletedTask;
    }

    public Task<IActivity> Send(IActivity activity, Api.ConversationReference reference, CancellationToken cancellationToken = default)
    {
        return Send<IActivity>(activity, reference, cancellationToken);
    }

    public async Task<TActivity> Send<TActivity>(TActivity activity, Api.ConversationReference reference, CancellationToken cancellationToken = default) where TActivity : IActivity
    {
        var client = new ApiClient(reference.ServiceUrl, Client, cancellationToken);

        activity.Conversation = reference.Conversation;
        activity.From = reference.Bot;
        activity.ChannelId = reference.ChannelId;

        // For targeted messages with an explicit Recipient (proactive sends), preserve it.
        // Otherwise, use the reference User from the conversation context.
        #pragma warning disable ExperimentalTeamsTargeted
        var isTargeted = activity.Recipient?.IsTargeted == true;

        if (isTargeted && reference.Conversation.Type?.IsPersonal == true)
        {
            throw new InvalidOperationException(
                "Targeted messages are not supported in personal (1:1) chats.");
        }

        if (!isTargeted)
        {
            activity.Recipient = reference.User;
        }

        if (activity.Id is not null && !activity.IsStreaming)
        {
            if (isTargeted)
            {
                await client
                .Conversations
                .Activities
                .UpdateTargetedAsync(reference.Conversation.Id, activity.Id, activity).ConfigureAwait(false);
            }
            else
            {
                await client
                .Conversations
                .Activities
                .UpdateAsync(reference.Conversation.Id, activity.Id, activity).ConfigureAwait(false);
            }

            return activity;
        }

        var res = isTargeted
            ? await client.Conversations.Activities.CreateTargetedAsync(reference.Conversation.Id, activity).ConfigureAwait(false)
            : await client.Conversations.Activities.CreateAsync(reference.Conversation.Id, activity).ConfigureAwait(false);
        #pragma warning restore ExperimentalTeamsTargeted

        activity.Id = res?.Id;
        return activity;
    }

    public IStreamer CreateStream(Api.ConversationReference reference, CancellationToken cancellationToken = default)
    {
        return new Stream()
        {
            Send = async activity =>
            {
                var res = await Send(activity, reference, cancellationToken).ConfigureAwait(false);
                return res;
            },
            Logger = Logger.Child("stream")
        };
    }

    public async Task<Response> Do(ActivityEvent @event, CancellationToken cancellationToken = default)
    {
        try
        {
            var @out = await Events(
                this,
                "activity",
                @event,
                cancellationToken
            ).ConfigureAwait(false);

            var res = (Response?)@out ?? throw new Exception("expected activity response");
            Logger.Debug(res);
            return res;
        }
        catch (Exception ex)
        {
            Logger.Error(ex);
            await Events(
                this,
                "error",
                new ErrorEvent() { Exception = ex },
                cancellationToken
            ).ConfigureAwait(false);

            return new Response(System.Net.HttpStatusCode.InternalServerError, ex.ToString());
        }
    }

    public async Task<IResult> Do(HttpContext httpContext, CancellationToken cancellationToken = default)
    {
        try
        {
            var request = httpContext.Request;
            var token = ExtractToken(request);
            var activity = await ParseActivity(request).ConfigureAwait(false);

            if (activity is null)
            {
                return Results.BadRequest("Missing activity");
            }

            // Require the token's serviceurl claim to match the activity's serviceUrl
            // (normalized, case-insensitive) when the activity specifies one. Mismatches
            // are logged server-side.
            if (!string.IsNullOrEmpty(activity.ServiceUrl))
            {
                var claimServiceUrl = token.Token.Payload.TryGetValue("serviceurl", out var serviceUrlClaim)
                    ? serviceUrlClaim as string
                    : null;

                if (!NormalizeServiceUrl(claimServiceUrl).Equals(NormalizeServiceUrl(activity.ServiceUrl), StringComparison.OrdinalIgnoreCase))
                {
                    Logger.Warn($"Rejecting activity {activity.Id}: serviceUrl '{activity.ServiceUrl}' does not match the token serviceurl claim '{claimServiceUrl}'.");
                    return Results.Unauthorized();
                }
            }

            var data = new Dictionary<string, object?>
            {
                ["Request.TraceId"] = httpContext.TraceIdentifier
            };

            foreach (var pair in httpContext.Items)
            {
                var key = pair.Key.ToString();

                if (key is null) continue;

                data[key] = pair.Value;
            }

            var res = await Do(new ActivityEvent()
            {
                Token = token,
                Activity = activity,
                Extra = data,
                Services = httpContext.RequestServices
            }, cancellationToken).ConfigureAwait(false);

            // convert response metadata to headers
            foreach (var (key, value) in res.Meta)
            {
                var str = value?.ToString();
                if (string.IsNullOrEmpty(str)) continue;
                httpContext.Response.Headers.Append($"X-Teams-{char.ToUpper(key[0]) + key[1..]}", str);
            }

            return Results.Json(
                res.Body,
                _jsonSerializerOptions,
                contentType: null,
                statusCode: (int)res.Status
            );
        }
        catch (Exception ex)
        {
            Logger.Error(ex);
            await Events(
                this,
                "error",
                new ErrorEvent() { Exception = ex },
                cancellationToken
            ).ConfigureAwait(false);

            return Results.Problem(detail: ex.Message, statusCode: 500);
        }
    }

    // Normalize a serviceUrl for comparison: null/empty becomes empty, and a single
    // trailing slash is trimmed so "https://host/teams" and "https://host/teams/" match.
    private static string NormalizeServiceUrl(string? serviceUrl) =>
        string.IsNullOrEmpty(serviceUrl) ? string.Empty :
            serviceUrl.EndsWith('/') ? serviceUrl[..^1] : serviceUrl;

    public JsonWebToken ExtractToken(HttpRequest httpRequest)
    {
        var authHeader = httpRequest.Headers.Authorization.FirstOrDefault() ?? throw new UnauthorizedAccessException();
        return new JsonWebToken(authHeader.Replace("Bearer ", ""));
    }

    public async Task<Activity?> ParseActivity(HttpRequest httpRequest)
    {
        httpRequest.EnableBuffering();

        if (httpRequest.Body.CanSeek)
        {
            // reset the stream position to the beginning in case it was read before
            httpRequest.Body.Position = 0;
        }

        using StreamReader sr = new(httpRequest.Body);
        var body = await sr.ReadToEndAsync().ConfigureAwait(false);
        Activity? activity = JsonSerializer.Deserialize<Activity>(body);

        return activity;
    }
}