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

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.Events;
using Microsoft.Teams.Apps.Plugins;
using Microsoft.Teams.Common.Logging;
using Microsoft.Teams.Common.Text;
using Microsoft.Teams.Plugins.AspNetCore.DevTools.Extensions;
using Microsoft.Teams.Plugins.AspNetCore.DevTools.Models;

namespace Microsoft.Teams.Plugins.AspNetCore.DevTools;

[Plugin]
[Obsolete("DevTools is deprecated and will be removed in a later version. Use Microsoft 365 Agents Playground instead.")]
public class DevToolsPlugin : IAspNetCorePlugin
{
    [AllowNull]
    [Dependency]
    public ILogger Logger { get; set; }

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

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

    public event EventFunction Events;

    internal MetaData MetaData => new() { Id = AppId, Name = AppName, Pages = _pages };
    internal readonly WebSocketCollection Sockets = [];

    private readonly ISenderPlugin _sender;
    private readonly IServiceProvider _services;
    private readonly IList<Page> _pages = [];
    private readonly TeamsDevToolsSettings _settings;

    public DevToolsPlugin(AspNetCorePlugin sender, IServiceProvider provider)
    {
        _sender = sender;
        _services = provider;
        _settings = provider.GetRequiredService<TeamsDevToolsSettings>();
    }

    public IApplicationBuilder Configure(IApplicationBuilder builder)
    {
        // Reject cross-origin WebSocket upgrades. The DevTools UI is always
        // loaded same-origin, so Origin must equal request Host; absent
        // Origin is rejected. Headers are inspected directly so this
        // middleware does not require UseWebSockets to run first.
        builder.Use(async (context, next) =>
        {
            var upgrade = context.Request.Headers["Upgrade"].ToString();
            if (string.Equals(upgrade, "websocket", StringComparison.OrdinalIgnoreCase))
            {
                var origin = context.Request.Headers["Origin"].ToString();
                var expectedOrigin = $"{context.Request.Scheme}://{context.Request.Host}";
                if (string.IsNullOrEmpty(origin) ||
                    !string.Equals(origin, expectedOrigin, StringComparison.OrdinalIgnoreCase))
                {
                    context.Response.StatusCode = StatusCodes.Status403Forbidden;
                    return;
                }
            }
            await next(context);
        });

        builder.UseWebSockets();

        builder.UseStaticFiles(new StaticFileOptions()
        {
            FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "web"),
            ServeUnknownFileTypes = true,
            RequestPath = "/devtools"
        });

        builder.Use(async (context, next) =>
        {
            try
            {
                await next(context).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Logger.Error(ex, "http error");
                throw new Exception(ex.Message, innerException: ex);
            }
        });

        return builder;
    }

    public DevToolsPlugin AddPage(Page page)
    {
        _pages.Add(page);
        Logger.Debug($"page '{page.Name}' added at '{page.Url}'");
        return this;
    }

    public Task OnInit(App app, CancellationToken cancellationToken = default)
    {
        var hostEnvironment = _services.GetService<IHostEnvironment>();
        var isProduction = hostEnvironment?.IsProduction()
            ?? string.Equals(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), "Production", StringComparison.OrdinalIgnoreCase);
        if (isProduction)
        {
            throw new InvalidOperationException(
                "Devtools plugin cannot be used in production environments. " +
                "Remove the devtools plugin from your app configuration.");
        }

        foreach (var page in _settings.Pages)
        {
            AddPage(page);
        }

        Logger.Warn(
            new StringBuilder()
                .Bold(
                    new StringBuilder()
                        .Yellow("⚠️  Devtools are not secure and should not be used production environments ⚠️")
                        .ToString()
                )
        );

        return Task.CompletedTask;
    }

    public Task OnStart(App app, CancellationToken cancellationToken = default)
    {
        var server = _services.GetRequiredService<IServer>();
        var addresses = server.Features.GetRequiredFeature<IServerAddressesFeature>().Addresses;

        foreach (var address in addresses)
        {
            Logger.Info($"Available at {address}/devtools");
        }

        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 async Task OnActivity(App app, ISenderPlugin sender, ActivityEvent @event, CancellationToken cancellationToken = default)
    {
        Logger.Debug("OnActivity");

        await Sockets.Emit(
            DevTools.Events.ActivityEvent.Received(
                @event.Activity,
                @event.Activity.Conversation
            ),
            cancellationToken
        ).ConfigureAwait(false);
    }

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

        await Sockets.Emit(
            DevTools.Events.ActivityEvent.Sent(
                @event.Activity,
                @event.Activity.Conversation
            ),
            cancellationToken
        ).ConfigureAwait(false);
    }

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

    public Task<Response> Do(ActivityEvent @event, CancellationToken cancellationToken = default)
    {
        return _sender.Do(@event, cancellationToken);
    }
}