namespace BotPages.Core;
using BotPages.Core.Abstractions;
using BotPages.Core.Context;
using BotPages.Core.Logging;
using BotPages.Core.Routing;
///
/// Основное приложение BotPages.
/// Управляет маршрутизацией, командами, middleware и страницами.
///
public sealed class BotPagesApp
{
private readonly IMessengerAdapterFactory _adapterFactory;
private readonly List _middlewares = new();
private readonly RoutesRegistry _routes = new();
private readonly CommandsRegistry _commands = new();
private readonly IStateStorage _state;
private readonly ILogger _logger;
private readonly NavigationService _navigation;
///
/// Создать приложение BotPages.
///
public BotPagesApp(IMessengerAdapterFactory adapterFactory, IStateStorage state, ILogger logger)
{
_adapterFactory = adapterFactory;
_state = state;
_logger = logger;
_navigation = new NavigationService(_routes);
}
///
/// Установить страницу по умолчанию.
///
public BotPagesApp AddDefaultPage() where TPage : SingletonPage
{
_navigation.AddDefaultPage();
return this;
}
///
/// Добавить middleware.
///
public BotPagesApp AddMiddleware(TMiddleware instance) where TMiddleware : IPageMiddleware
{
_middlewares.Add(instance);
return this;
}
///
/// Зарегистрировать команду, ведущую на страницу.
///
public BotPagesApp MapCommand(string commandTemplate) where TPage : Page
{
_commands.Map(commandTemplate);
return this;
}
///
/// Зарегистрировать команду с кастомным обработчиком.
///
public BotPagesApp MapCommand(string template, Func handler)
{
_commands.Map(template, handler);
return this;
}
///
/// Зарегистрировать маршрут для страницы.
///
public BotPagesApp MapRoute(string template) where TPage : Page
{
_routes.Map(template);
return this;
}
///
/// Обработать входящее обновление.
///
public async Task HandleUpdateAsync(UpdateContext update, CancellationToken ct)
{
var ctx = await CreatePageContextAsync(update, ct);
// Команды выше событий страниц
if (update.Kind == UpdateKind.Text && update.Text is not null && update.Text.StartsWith("/"))
{
if (_commands.TryDispatch(ctx, update.Text, ct, out var dispatched) && dispatched is not null)
{
_logger.Log(LogLevel.Info, $"Command '{update.Text}' dispatched.");
await dispatched;
return;
}
}
// Конвейер middleware
var pipeline = BuildPipeline(ctx, async () =>
{
await DispatchToPageAsync(ctx, update, ct);
});
await pipeline();
}
private Func BuildPipeline(PageContext ctx, Func terminal)
{
Func next = terminal;
foreach (var mw in _middlewares.AsEnumerable().Reverse())
{
var prev = next;
next = () => mw.InvokeAsync(ctx, prev, _currentCt);
}
return next;
}
// Технические поля для конвейера
private CancellationToken _currentCt;
///
/// Создать контекст страницы для текущего обновления.
///
private async Task CreatePageContextAsync(UpdateContext update, CancellationToken ct)
{
_currentCt = ct;
var sessionKey = new CompositeSessionKey(update.MessengerType, update.Chat.Id, update.User.Id);
var ctx = new PageContext
{
Update = update,
SessionKey = sessionKey,
StateStorage = _state,
Navigation = _navigation,
Adapter = _adapterFactory.Resolve(update.MessengerType),
};
return await Task.FromResult(ctx);
}
///
/// Отправить обновление на текущую страницу.
///
private async Task DispatchToPageAsync(PageContext ctx, UpdateContext update, CancellationToken ct)
{
var page = ResolveCurrentPage(ctx);
if (page is null)
{
await ctx.Navigation.GoToHome(ctx, ct);
return;
}
try
{
await page.OnUpdate(ctx, update, ct);
if (update.Kind.HasFlag(UpdateKind.Text) && update.Text is not null) await page.OnText(ctx, update.Text, ct);
if (update.Kind.HasFlag(UpdateKind.Button) && update.Text is not null) await page.OnButton(ctx, update.Text, ct);
if (update.Kind.HasFlag(UpdateKind.File) && update.Files.Count > 0) await page.OnFile(ctx, update.Files, ct);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Critical, "Unhandled page error.", ex);
await page.OnError(ctx, ex, ct);
}
}
///
/// Определить текущую страницу.
///
private Page? ResolveCurrentPage(PageContext ctx)
=> _navigation.ResolveCurrentPage(ctx);
}