using Microsoft.Extensions.Logging; using PARR.Core.Services.HelpDocument.Interfaces; using PARR.Domain.DTOs.HelpDocument; using PARR.Domain.Settings; using System.Web; namespace PARR.Core.Services.HelpDocument.Implementations { /// /// Получение справочной документации из GitLab /// internal class GitLabHelpProvider : IHelpDocumentProvider { private readonly HttpClient _httpClient; private readonly HelpDocumentSettings _helpDocumentSettings; private readonly ILogger _logger; public GitLabHelpProvider(HttpClient httpClient, HelpDocumentSettings helpDocumentSettings, ILogger logger) { _httpClient = httpClient; _helpDocumentSettings = helpDocumentSettings; _logger = logger; // Базовая настройка http _httpClient = httpClient; if (_helpDocumentSettings.GitLabSettings == null) { _logger.LogCritical("Инициализация провайдера справки провалена: не задана секция GitLabSettings"); throw new ArgumentNullException(nameof(_helpDocumentSettings.GitLabSettings), "Не заданы настройки подключения к GitLab"); } _httpClient.BaseAddress = new Uri(_helpDocumentSettings.GitLabSettings.BaseUrl); _httpClient.DefaultRequestHeaders.Add("PRIVATE-TOKEN", _helpDocumentSettings.GitLabSettings.Token); } public async Task GetDocumentAsync(string helpId) { //1. Формируем путь к файлу var filePath = $"{helpId}.md"; // 2. GitLab API требует, чтобы относительный путь к файлу в URL был закодирован (экранирован) var urlEncodedPath = HttpUtility.UrlEncode(filePath); // 3. Собираем эндпоинт для получения RAW контента файла string relativeRequestUrl = $"projects/{_helpDocumentSettings.GitLabSettings!.ProjectId}/repository/files/{urlEncodedPath}/raw?ref={_helpDocumentSettings.GitLabSettings.DefaultBranch}"; try { // Создаем URI строго с флагом UriKind.Relative. Это заставит HttpClient отправить %2F в сыром виде! var relativeUri = new Uri(relativeRequestUrl, UriKind.Relative); _logger.LogDebug("Отправка HTTP GET запроса к GitLab API. Относительный URI: {RelativeUri}", relativeRequestUrl); var response = await _httpClient.GetAsync(relativeUri); // Если файла нет (404) или токен не подошел (401/403) if (!response.IsSuccessStatusCode) { _logger.LogWarning("Документ справки не найден или недоступен в GitLab. HelpId: {HelpId}, StatusCode: {StatusCode}", helpId, response.StatusCode); return null; } _logger.LogDebug("Файл справки успешно получен из GitLab. Размер ответа: {ContentLength} байт", response.Content.Headers.ContentLength); // Получаем весь текст файла (и шапку, и контент) string rawFileContent = await response.Content.ReadAsStringAsync(); // 4. Парсим файл var result = ParseMarkdownFile(helpId, rawFileContent); _logger.LogDebug("Документ справки успешно загружен и распарсен. HelpId: {HelpId}, Title: '{Title}'", helpId, result.Title); var editUrl = GenerateEditUrl(helpId); return result with { EditUrl = editUrl }; } catch (Exception ex) { _logger.LogError(ex, "Ошибка при запросе справки {HelpId} из GitLab", helpId); return null; } } private string? GenerateEditUrl(string helpId) { if (_helpDocumentSettings.GitLabSettings == null) return null; var repoPath = _helpDocumentSettings.GitLabSettings.RepositoryPath; var branch = _helpDocumentSettings.GitLabSettings.DefaultBranch ?? "main"; return $"{repoPath}/-/edit/{branch}/{helpId}.md"; } private HelpDocumentResult ParseMarkdownFile(string helpId, string rawContent) { _logger.LogDebug("Начало парсинга структуры Markdown для HelpId: {HelpId}", helpId); if (string.IsNullOrWhiteSpace(rawContent)) { _logger.LogWarning("Получен пустой контент для HelpId: {HelpId}", helpId); return new HelpDocumentResult { HelpId = helpId }; } // Стандартное разделение строк с поддержкой разных типов окончаний (Windows/Linux) var lines = rawContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); var title = string.Empty; string? prevId = null; string? nextId = null; var contentStartIndex = 0; var isInsideFrontMatter = false; var frontMatterClosed = false; for (int i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); // Ищем маркеры начала и конца метаданных "---" if (line == "---") { if (!isInsideFrontMatter && !frontMatterClosed) { isInsideFrontMatter = true; _logger.LogDebug("Обнаружено начало блока метаданных (Front Matter) на строке {LineIndex}", i); continue; } else if (isInsideFrontMatter) { isInsideFrontMatter = false; frontMatterClosed = true; contentStartIndex = i + 1; // Контент начинается строго со следующей строки _logger.LogDebug("Обнаружен конец блока метаданных на строке {LineIndex}", i); break; } } // Читаем свойства внутри блока метаданных if (isInsideFrontMatter) { if (line.StartsWith("title:", StringComparison.OrdinalIgnoreCase)) { title = line.Substring("title:".Length).Trim(); _logger.LogDebug("В метаданных найден Title: '{Title}'", title); } // Парсим Предыдущий шаг else if (line.StartsWith("prev_id:", StringComparison.OrdinalIgnoreCase)) { prevId = line.Substring("prev_id:".Length).Trim(); _logger.LogDebug("В метаданных найден PrevId: '{PrevId}'", prevId); } // Парсим Следующий шаг else if (line.StartsWith("next_id:", StringComparison.OrdinalIgnoreCase)) { nextId = line.Substring("next_id:".Length).Trim(); _logger.LogDebug("В метаданных найден NextId: '{NextId}'", nextId); } // Тут в будущем можно парсить другие поля, например: icon, min_role и т.д. } } string cleanContent; if (frontMatterClosed) { // Собираем обратно чистый контент, исключая строки с шапкой cleanContent = string.Join(Environment.NewLine, lines.Skip(contentStartIndex)); } else { // Если шапки "---" вообще не было в файле, берем весь текст как есть _logger.LogDebug("Блок метаданных '---' не найден в файле. Весь текст перенесен в Content."); cleanContent = rawContent; title = Path.GetFileNameWithoutExtension(helpId); // Дефолтный заголовок из имени файла } return new HelpDocumentResult { HelpId = helpId, Title = title, Content = cleanContent, PrevId = prevId, NextId = nextId }; } } }