using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Mcp.Dtos;
using Sozsoft.Mcp.Services;
using Sozsoft.Platform;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms.Select;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Mcp;
///
/// Rapor kategorisi ve DynamicReport sablonu uretimi. Okuma depodan, yazma ekranlarin
/// C/U/D sozlesmesinden () gecer; boylece ekran yetkisi,
/// kiraci filtresi ve seed senkronizasyonu ekrandaki kayitla ayni kalir.
///
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public partial class McpReportAppService(
IMcpServerConfigProvider serverConfigProvider,
IListFormDataAppService listFormDataAppService,
IMcpListFormAccessFilter accessFilter,
IRepository categoryRepository,
IRepository templateRepository,
IRepository listFormRepository,
IRepository listFormFieldRepository,
IMcpDevRequestTimesheetRecorder timesheetRecorder)
: McpAppService(serverConfigProvider), IMcpReportAppService
{
private const string CategoriesListFormCode = PlatformConsts.AppCodes.Reports.Categories;
private const string TemplatesListFormCode = PlatformConsts.AppCodes.Reports.ReportTemplates;
private const string ActiveStatus = "active";
private const string DefaultButtonText = "App.Platform.View";
[GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")]
private static partial Regex PlaceholderRegex();
public async Task> GetReportTemplatesAsync(string? search)
{
if (await EnsureCanReadAsync(McpToolNames.ListReportTemplates) is { } problem)
{
return problem;
}
var categories = await categoryRepository.GetListAsync();
var categoryNames = categories.ToDictionary(x => x.Id, x => x.Name);
var templates = (await templateRepository.GetListAsync())
.Where(x => string.IsNullOrWhiteSpace(search) ||
x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) ||
(x.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false))
.OrderBy(x => x.Name)
.Select(x => new McpReportTemplateSummaryDto
{
Id = x.Id,
Name = x.Name,
Description = x.Description,
Category = categoryNames.GetValueOrDefault(x.CategoryId, string.Empty),
Status = x.Status
})
.ToList();
return new McpReportCatalogDto
{
Categories = [.. categories.OrderBy(x => x.Name).Select(ToDto)],
Templates = templates
};
}
public async Task> GetReportTemplateAsync(string template)
{
if (await EnsureCanReadAsync(McpToolNames.GetReportTemplate) is { } problem)
{
return problem;
}
var entity = await FindTemplateAsync(template);
if (entity is null)
{
return Problem(McpErrorCodes.ReportTemplateNotFound, template);
}
return await ToDtoAsync(entity, null, null);
}
public async Task> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input)
{
Check.NotNull(input, nameof(input));
const string toolName = McpToolNames.SaveReportCategory;
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(input.Name))
{
return Problem(McpErrorCodes.ReportNameRequired);
}
var name = input.Name.Trim();
var existing = await categoryRepository.FirstOrDefaultAsync(x => x.Name == name);
var values = new Dictionary
{
["Name"] = name,
["Description"] = input.Description ?? existing?.Description,
["Icon"] = input.Icon ?? existing?.Icon
};
var saved = existing is null
? await InsertAsync(toolName, CategoriesListFormCode, values)
: await UpdateAsync(toolName, CategoriesListFormCode, existing.Id, values);
if (!saved.TryGetValue(out var id, out var rejected))
{
return rejected;
}
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name);
var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id);
return category is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(category);
}
public async Task> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input)
{
Check.NotNull(input, nameof(input));
const string toolName = McpToolNames.SaveReportTemplate;
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(input.Name))
{
return Problem(McpErrorCodes.ReportNameRequired);
}
var name = input.Name.Trim();
var existing = await templateRepository.FirstOrDefaultAsync(x => x.Name == name);
var htmlContent = string.IsNullOrWhiteSpace(input.HtmlContent) ? existing?.HtmlContent : input.HtmlContent;
if (string.IsNullOrWhiteSpace(htmlContent))
{
return Problem(McpErrorCodes.ReportHtmlRequired);
}
var categoryId = existing?.CategoryId;
if (!string.IsNullOrWhiteSpace(input.Category))
{
categoryId = (await FindCategoryAsync(input.Category))?.Id;
if (categoryId is null)
{
return Problem(McpErrorCodes.ReportCategoryNotFound, input.Category);
}
}
if (categoryId is null)
{
return Problem(McpErrorCodes.ReportCategoryRequired);
}
if (!(await FindListFormAsync(input.ListFormCode)).TryGetValue(out var listForm, out var notFound))
{
return notFound;
}
if (listForm is not null && await CheckPlaceholdersExistAsync(listForm.ListFormCode, htmlContent) is { } unknown)
{
return unknown;
}
var values = new Dictionary
{
["Name"] = name,
["Description"] = input.Description ?? existing?.Description,
["HtmlContent"] = htmlContent,
["CategoryId"] = categoryId,
["Status"] = input.Status ?? existing?.Status ?? ActiveStatus
};
var saved = existing is null
? await InsertAsync(toolName, TemplatesListFormCode, values)
: await UpdateAsync(toolName, TemplatesListFormCode, existing.Id, values);
if (!saved.TryGetValue(out var id, out var rejected))
{
return rejected;
}
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name);
var template = await templateRepository.FirstOrDefaultAsync(x => x.Id == id);
if (template is null)
{
return Problem(McpErrorCodes.ReportTemplateNotFound, name);
}
return await ToDtoAsync(template, listForm, input.ButtonText);
}
public async Task DeleteReportTemplateAsync(string template)
{
const string toolName = McpToolNames.DeleteReportTemplate;
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
var entity = await FindTemplateAsync(template);
if (entity is null)
{
return Problem(McpErrorCodes.ReportTemplateNotFound, template);
}
if (!(await BuildRequestAsync(toolName, TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)).TryGetValue(out var request, out var denied))
{
return denied;
}
await listFormDataAppService.PostDeleteAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name);
return null;
}
///
/// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin
/// uygulamada goremedigi sablonu gostermez.
///
private async Task EnsureCanReadAsync(string toolName)
{
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read);
return await CheckPermissionAsync(toolName, permission ?? TemplatesListFormCode);
}
private async Task> InsertAsync(string toolName, string listFormCode, Dictionary values)
{
if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Create, values, null)).TryGetValue(out var request, out var problem))
{
return problem;
}
var key = await listFormDataAppService.PostInsertAsync(request);
// Insert ucu her veri kaynaginda anahtari dondurmez; bu durumda tekil ad uzerinden bulunur.
if (Guid.TryParse(((object?)key)?.ToString(), out var id))
{
return id;
}
var name = (string)values["Name"]!;
return listFormCode == CategoriesListFormCode
? (await categoryRepository.FirstAsync(x => x.Name == name)).Id
: (await templateRepository.FirstAsync(x => x.Name == name)).Id;
}
private async Task> UpdateAsync(string toolName, string listFormCode, Guid id, Dictionary values)
{
if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Update, values, id)).TryGetValue(out var request, out var problem))
{
return problem;
}
await listFormDataAppService.PostUpdateAsync(request);
return id;
}
///
/// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki
/// istemci hangi yetkinin eksik oldugunu gorsun.
///
private async Task> BuildRequestAsync(
string toolName,
string listFormCode,
AuthorizationTypeEnum operation,
Dictionary? values,
Guid? key)
{
var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation);
if (permission is not null && await CheckPermissionAsync(toolName, permission) is { } denied)
{
return denied;
}
return new DataRequestDto
{
ListFormCode = listFormCode,
Data = JsonSerializer.Serialize(values ?? []),
Keys = key is null ? [] : [key.Value]
};
}
private async Task FindTemplateAsync(string template)
{
if (string.IsNullOrWhiteSpace(template))
{
return null;
}
var value = template.Trim();
return Guid.TryParse(value, out var id)
? await templateRepository.FirstOrDefaultAsync(x => x.Id == id)
: await templateRepository.FirstOrDefaultAsync(x => x.Name == value);
}
private async Task FindCategoryAsync(string category)
{
var value = category.Trim();
return Guid.TryParse(value, out var id)
? await categoryRepository.FirstOrDefaultAsync(x => x.Id == id)
: await categoryRepository.FirstOrDefaultAsync(x => x.Name == value);
}
/// Kod verilmediyse null; verilen kodun ekrani yoksa sorun.
private async Task> FindListFormAsync(string? listFormCode)
{
if (string.IsNullOrWhiteSpace(listFormCode))
{
return default(ListForm);
}
var code = listFormCode.Trim();
var listForm = await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code);
return listForm is null ? Problem(McpErrorCodes.ReportListFormNotFound, code) : listForm;
}
///
/// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin
/// diye kayitta reddedilir.
///
private async Task CheckPlaceholdersExistAsync(string listFormCode, string htmlContent)
{
var fieldNames = (await listFormFieldRepository.GetListAsync(x => x.ListFormCode == listFormCode))
.Select(x => x.FieldName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var unknown = GetPlaceholders(htmlContent).Where(x => !fieldNames.Contains(x)).ToList();
return unknown.Count == 0
? null
: Problem(McpErrorCodes.ReportPlaceholdersUnknown, listFormCode, string.Join(", ", unknown));
}
private static List GetPlaceholders(string htmlContent)
=> [.. PlaceholderRegex().Matches(htmlContent ?? string.Empty)
.Select(x => x.Groups[1].Value)
.Distinct(StringComparer.OrdinalIgnoreCase)];
private async Task ToDtoAsync(ReportTemplate entity, ListForm? listForm, string? buttonText)
{
var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == entity.CategoryId);
var viewUrl = $"/admin/reports/DynamicReport/view/{entity.Id}";
return new McpReportTemplateDto
{
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
Category = category?.Name ?? string.Empty,
CategoryId = entity.CategoryId,
Status = entity.Status,
HtmlContent = entity.HtmlContent,
Placeholders = GetPlaceholders(entity.HtmlContent),
ViewUrl = viewUrl,
RowButton = listForm is null ? null : new McpReportRowButtonDto
{
Text = string.IsNullOrWhiteSpace(buttonText) ? DefaultButtonText : buttonText,
Hint = string.IsNullOrWhiteSpace(buttonText) ? DefaultButtonText : buttonText,
Url = $"{viewUrl}/{listForm.ListFormCode}?recordId=@{listForm.KeyFieldName}",
AuthName = listForm.ListFormCode
}
};
}
private static McpReportCategoryDto ToDto(ReportCategory category) => new()
{
Id = category.Id,
Name = category.Name,
Description = category.Description,
Icon = category.Icon
};
}