using System.Text.RegularExpressions;
using Dpz.Core.Entity.Base.ExpressTreeQuery;
using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.Response;

namespace Dpz.Core.Service.RepositoryServiceImpl;

public class CodeFileSystemEntryService(
    IRepository<CodeFileSystemEntry> repository,
    IConfiguration configuration,
    ILogger<CodeFileSystemEntryService> logger,
    IFusionCache fusionCache
) : ICodeFileSystemEntryService
{
    private static readonly TimeSpan CacheDefaultExpiration = TimeSpan.FromDays(7);

    private readonly Lazy<CodeViewOption> _codeViewCfg = new(() =>
    {
        var codeView = configuration.GetSection("CodeView").Get<CodeViewOption>();
        return codeView ?? throw new InvalidConfigurationException("CodeView 配置无效");
    });

    public async Task<CodeFileSystemEntryResponse?> FindByPathAsync(
        IReadOnlyCollection<string>? pathSegments,
        CancellationToken cancellationToken = default
    )
    {
        var segments = NormalizeSegments(pathSegments);
        var cacheKey = BuildPathKey(segments);
        return await GetOrSetCacheAsync<CodeFileSystemEntryResponse?>(
            nameof(FindByPathAsync),
            "Path=" + cacheKey,
            ct => FindByPathCoreAsync(segments, ct),
            cancellationToken: cancellationToken
        );
    }

    public async Task<CodeFileSystemEntryResponse?> FindByPathWithoutCacheAsync(
        IReadOnlyCollection<string>? pathSegments,
        CancellationToken cancellationToken = default
    )
    {
        var segments = NormalizeSegments(pathSegments);
        return await FindByPathCoreAsync(segments, cancellationToken);
    }

    public async Task<List<CodeFileSystemEntryResponse>> GetChildrenAsync(
        IReadOnlyCollection<string>? parentPathSegments,
        CancellationToken cancellationToken = default
    )
    {
        var segments = NormalizeSegments(parentPathSegments);
        var cacheKey = BuildPathKey(segments);
        return await GetOrSetCacheAsync<List<CodeFileSystemEntryResponse>>(
            nameof(GetChildrenAsync),
            "Parent=" + cacheKey,
            async ct =>
            {
                var filter = Builders<CodeFileSystemEntry>.Filter.Eq(
                    x => x.ParentPathSegments,
                    segments
                );
                var entries = await repository.SearchFor(filter).ToListAsync(ct);
                return await MapToResponseListAsync(entries, ct);
            },
            cancellationToken: cancellationToken
        );
    }

    public async Task<List<CodeFileSystemEntryResponse>> SearchAsync(
        string keyword,
        CancellationToken cancellationToken = default
    )
    {
        if (string.IsNullOrWhiteSpace(keyword))
        {
            return [];
        }

        return await GetOrSetCacheAsync<List<CodeFileSystemEntryResponse>>(
            nameof(SearchAsync),
            "Keyword=" + keyword,
            async ct =>
            {
                var filter = Builders<CodeFileSystemEntry>.Filter.Regex(
                    x => x.Name,
                    new BsonRegularExpression(BuildSearchPattern(keyword), "i")
                );
                var entries = await repository.SearchFor(filter).ToListAsync(ct);
                return await MapToResponseListAsync(entries, ct);
            },
            cancellationToken: cancellationToken
        );
    }

    /// <inheritdoc />
    public async Task<List<CodeContentSearchHitResponse>> SearchContentAsync(
        string query,
        int? maxResults = null,
        CancellationToken cancellationToken = default
    )
    {
        var pattern = CodeContentSearch.BuildEscapedPattern(query);
        if (pattern == null)
        {
            return [];
        }

        // 字面量转义后再做不区分大小写的正则,避免用户输入变成 Mongo 正则炸弹
        var limit = CodeContentSearch.ClampMaxResults(maxResults);
        var filter = Builders<CodeFileSystemEntry>.Filter.And(
            Builders<CodeFileSystemEntry>.Filter.Eq(x => x.IsDirectory, false),
            Builders<CodeFileSystemEntry>.Filter.Eq(
                x => x.CodeFileContentType,
                CodeFileContentType.Text
            ),
            Builders<CodeFileSystemEntry>.Filter.Regex(
                x => x.FileContent,
                new BsonRegularExpression(pattern, "i")
            )
        );

        var entries = await repository
            .SearchFor(filter)
            .Limit(limit)
            .ToListAsync(cancellationToken);

        return entries
            .Select(entry => new CodeContentSearchHitResponse
            {
                PathSegments = entry.PathSegments.ToList(),
                Name = entry.Name,
                Path = string.Join('/', entry.PathSegments),
                Snippet = CodeContentSearch.BuildSnippet(entry.FileContent, query),
            })
            .ToList();
    }

    /// <summary>
    /// 将用户输入转换为安全的 MongoDB 正则:
    /// `*` 视为任意字符、`?` 视为单个字符通配符,其余正则元字符按字面量匹配;
    /// 含通配符时整体锚定首尾(glob 语义),否则保持子串匹配。
    /// </summary>
    private static string BuildSearchPattern(string keyword)
    {
        var hasWildcard = keyword.Contains('*') || keyword.Contains('?');
        var pattern = ApplicationTools.WildcardToRegexPattern(keyword);
        return hasWildcard ? $"^{pattern}$" : pattern;
    }

    public async Task<IPagedList<CodeFileSystemEntryListResponse>> GetPagedListAsync(
        CodeFlatRequest request,
        CancellationToken cancellationToken = default
    )
    {
        // 生成查询条件
        var filter = GenerateConditionFilter.GenerateConditionExpressionTree<
            CodeFileSystemEntry,
            CodeFlatRequest
        >(request);

        var query = repository.SearchFor(filter);

        // 应用排序
        if (request.SortField.HasValue)
        {
            query = request.SortField.Value switch
            {
                CodeFileSortField.Size => request.IsDescending
                    ? query.OrderByDescending(x => x.Size)
                    : query.OrderBy(x => x.Size),
                CodeFileSortField.CreatedTime => request.IsDescending
                    ? query.OrderByDescending(x => x.CreatedTime)
                    : query.OrderBy(x => x.CreatedTime),
                CodeFileSortField.LastWriteTime => request.IsDescending
                    ? query.OrderByDescending(x => x.LastWriteTime)
                    : query.OrderBy(x => x.LastWriteTime),
                CodeFileSortField.LastUpdateTime => request.IsDescending
                    ? query.OrderByDescending(x => x.LastUpdateTime)
                    : query.OrderBy(x => x.LastUpdateTime),
                CodeFileSortField.AiAnalyzeTime => request.IsDescending
                    ? query.OrderByDescending(x => x.AiAnalyzeTime)
                    : query.OrderBy(x => x.AiAnalyzeTime),
                _ => query,
            };
        }

        // 执行分页查询
        var entityPagedList = await query
            .Select(x => new CodeFileSystemEntryListResponse
            {
                Id = x.Id.ToString(),
                PathSegments = x.PathSegments,
                Name = x.Name,
                ParentPathSegments = x.ParentPathSegments,
                IsDirectory = x.IsDirectory,
                Extension = x.Extension,
                Size = x.Size,
                Hash = x.Hash,
                CodeFileContentType = x.CodeFileContentType,
                CodeLanguage = x.CodeLanguage,
                Tags = x.Tags,
                CreatedTime = x.CreatedTime,
                LastWriteTime = x.LastWriteTime,
                LastUpdateTime = x.LastUpdateTime,
                Description = x.Description,
                AiAnalyzeTime = x.AiAnalyzeTime,
                AiAnalyzeHash = x.AiAnalyzeHash,
            })
            .ToPagedListAsync(request.PageIndex, request.PageSize, cancellationToken);

        entityPagedList.ForEach(x =>
            x.CodeLanguage = ResolveCodeLanguage(x.IsDirectory, x.Extension, x.Name)
        );

        return entityPagedList;
    }

    public async Task<List<string[]>> GetAllDirectoriesAsync(
        string[]? pathSegments = null,
        CancellationToken cancellationToken = default
    )
    {
        var normalizedPath = NormalizeSegments(pathSegments);
        var pathKey = BuildPathKey(normalizedPath);

        return await GetOrSetCacheAsync<List<string[]>>(
            nameof(GetAllDirectoriesAsync),
            "Path=" + pathKey,
            async ct =>
            {
                var filterBuilder = Builders<CodeFileSystemEntry>.Filter;
                var filter = filterBuilder.Eq(x => x.IsDirectory, true);

                // 如果指定了路径,则只查询该路径及其所有子目录
                if (normalizedPath.Count > 0)
                {
                    // 查询所有 PathSegments 以 normalizedPath 开头的目录
                    // 先查询所有目录,然后在内存中过滤
                    var allDirectories = await repository
                        .SearchFor(filter)
                        .ToListAsync(cancellationToken: ct);

                    var filteredDirectories = allDirectories
                        .Where(x =>
                            x.PathSegments.Count >= normalizedPath.Count
                            && x.PathSegments.Take(normalizedPath.Count)
                                .SequenceEqual(normalizedPath)
                        )
                        .ToList();

                    return filteredDirectories
                        .Select(x => x.PathSegments.ToArray())
                        .OrderBy(x => x.Length)
                        .ThenBy(x => string.Join("/", x))
                        .ToList();
                }

                // 没有指定路径,返回所有目录
                var directories = await repository
                    .SearchFor(filter)
                    .ToListAsync(cancellationToken: ct);

                return directories
                    .Select(x => x.PathSegments.ToArray())
                    .OrderBy(x => x.Length)
                    .ThenBy(x => string.Join("/", x))
                    .ToList();
            },
            cancellationToken: cancellationToken
        );
    }

    public async Task<bool> SaveDescriptionAsync(
        string[]? path,
        string name,
        string? description,
        CancellationToken cancellationToken = default
    )
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return false;
        }

        var segments = NormalizeSegments(path).ToList();
        segments.Add(name);
        var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
        var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
        if (entry == null)
        {
            return false;
        }

        entry.Description = description;
        entry.LastUpdateTime = DateTime.Now;
        await repository.UpdateAsync(entry, cancellationToken);
        await InvalidateEntryCachesAsync(entry);
        await InvalidateSearchCacheAsync(name);
        return true;
    }

    public async Task SaveAiAnalyzeResultAsync(
        string[]? path,
        string name,
        string analyzeResult,
        string? analyzedHash = null,
        CancellationToken cancellationToken = default
    )
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return;
        }

        var segments = NormalizeSegments(path).ToList();
        segments.Add(name);
        var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
        var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
        if (entry == null)
        {
            return;
        }

        entry.AiAnalyzeResult = analyzeResult;
        entry.AiAnalyzeHash = analyzedHash ?? entry.Hash;
        entry.AiAnalyzeTime = DateTime.Now;
        entry.LastUpdateTime = DateTime.Now;
        await repository.UpdateAsync(entry, cancellationToken);
        await InvalidateEntryCachesAsync(entry);
        await InvalidateSearchCacheAsync(name);
    }

    public async Task<bool> ShouldAnalyzeAsync(
        string[]? path,
        string? name,
        CodeContainer? codeContainer,
        CancellationToken cancellationToken = default
    )
    {
        var useAiAnalyze = configuration.GetValue("CodeUseAIAnalyze", false);
        if (!useAiAnalyze)
        {
            return false;
        }

        if (
            string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(codeContainer?.CodeContent)
        )
        {
            return false;
        }

        // 检查代码行数,少于50行不分析
        var lineCount = codeContainer
            .CodeContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None)
            .Length;
        if (lineCount < 50)
        {
            return false;
        }

        if (name.Contains(".min"))
        {
            return false;
        }

        var language = codeContainer.Language?.ToLowerInvariant();
        var isSupportedLanguage = language is "csharp" or "javascript" or "typescript";
        if (!isSupportedLanguage)
        {
            return false;
        }

        var segments = NormalizeSegments(path).ToList();
        var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
        var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
        if (entry == null)
        {
            return false;
        }

        if (string.IsNullOrWhiteSpace(entry.AiAnalyzeResult))
        {
            return true;
        }

        if (!string.Equals(entry.AiAnalyzeHash, entry.Hash, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        return false;
    }

    private async Task InvalidateEntryCachesAsync(CodeFileSystemEntry entry)
    {
        var entryKey = BuildPathKey(entry.PathSegments);
        await fusionCache.RemoveAsync(BuildCacheKey(nameof(FindByPathAsync), "Path=" + entryKey));

        var parentKey = BuildPathKey(entry.ParentPathSegments);
        await fusionCache.RemoveAsync(
            BuildCacheKey(nameof(GetChildrenAsync), "Parent=" + parentKey)
        );
    }

    private async Task<CodeFileSystemEntryResponse?> FindByPathCoreAsync(
        List<string> segments,
        CancellationToken cancellationToken
    )
    {
        // 先尝试精确匹配(区分大小写)
        var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
        var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);

        if (entry == null && segments.Count > 0)
        {
            // 如果精确匹配失败,则构建不区分大小写的正则过滤器
            var regexes = segments
                .Select(x => new BsonRegularExpression($"^{Regex.Escape(x)}$", "i"))
                .ToArray();
            var caseInsensitiveFilter = new BsonDocumentFilterDefinition<CodeFileSystemEntry>(
                new BsonDocument
                {
                    {
                        nameof(CodeFileSystemEntry.PathSegments),
                        new BsonDocument
                        {
                            { "$size", segments.Count },
                            { "$all", new BsonArray(regexes) },
                        }
                    },
                }
            );

#if DEBUG
            var serializerRegistry = BsonSerializer.SerializerRegistry;
            var documentSerializer = serializerRegistry.GetSerializer<CodeFileSystemEntry>();
            var serializerArgs = new RenderArgs<CodeFileSystemEntry>(
                documentSerializer,
                serializerRegistry
            );
            var bsonDocumentQuery = caseInsensitiveFilter.Render(serializerArgs);
            logger.LogInformation(
                "构建查询条件: {BsonDocumentQuery}",
                bsonDocumentQuery.ToString()
            );
#endif
            entry = await repository
                .SearchFor(caseInsensitiveFilter)
                .FirstOrDefaultAsync(cancellationToken);
        }

        if (entry == null)
        {
            return null;
        }

        if (!entry.IsDirectory)
        {
            entry = await FixPreviewContentAsync(entry, cancellationToken);
        }

        var readmeContent = entry.IsDirectory
            ? await ResolveReadmeContentAsync(entry.PathSegments, cancellationToken)
            : null;
        return MapToResponse(entry, readmeContent);
    }

    private async Task InvalidateSearchCacheAsync(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return;
        }

        await fusionCache.RemoveAsync(BuildCacheKey(nameof(SearchAsync), "Keyword=" + name));
    }

    private async Task<T> GetOrSetCacheAsync<T>(
        string methodName,
        string parameters,
        Func<CancellationToken, Task<T>> factory,
        CancellationToken cancellationToken = default
    )
    {
        return await fusionCache.GetOrSetAsync<T>(
            BuildCacheKey(methodName, parameters),
            async (context, factoryCancellationToken) =>
            {
                context.Tags =
                [
                    GeneratedCodeFileSystemEntryServiceCacheMetadata.DefaultPrefix,
                    GeneratedCodeFileSystemEntryServiceCacheMetadata.GetMethodTag(methodName),
                ];
                return await factory(factoryCancellationToken);
            },
            options =>
                options
                    .SetDuration(CacheDefaultExpiration)
                    .SetFailSafe(true, CacheDefaultExpiration),
            token: cancellationToken
        );
    }

    private static string BuildCacheKey(string methodName, string parameters)
    {
        return string.Concat(
            GeneratedCodeFileSystemEntryServiceCacheMetadata.DefaultPrefix,
            ":",
            methodName,
            ":",
            parameters
        );
    }

    private static List<string> NormalizeSegments(IReadOnlyCollection<string>? segments)
    {
        return segments?.Where(x => !string.IsNullOrWhiteSpace(x)).ToList() ?? [];
    }

    private CodeFileSystemEntryResponse MapToResponse(
        CodeFileSystemEntry entry,
        string? readmeContent
    )
    {
        return new CodeFileSystemEntryResponse
        {
            Id = entry.Id.ToString(),
            PathSegments = entry.PathSegments.ToList(),
            Name = entry.Name,
            ParentPathSegments = entry.ParentPathSegments.ToList(),
            IsDirectory = entry.IsDirectory,
            Extension = entry.Extension,
            Size = entry.Size,
            Hash = entry.Hash,
            CodeFileContentType = entry.CodeFileContentType,
            FileContent = entry.FileContent,
            CodeLanguage = ResolveCodeLanguage(entry.IsDirectory, entry.Extension, entry.Name),
            Tags = entry.Tags.ToList(),
            CreatedTime = entry.CreatedTime,
            LastWriteTime = entry.LastWriteTime,
            LastUpdateTime = entry.LastUpdateTime,
            Description = entry.Description,
            AiAnalyzeResult = entry.AiAnalyzeResult,
            AiAnalyzeTime = entry.AiAnalyzeTime,
            AiAnalyzeHash = entry.AiAnalyzeHash,
            ReadmeContent = readmeContent,
        };
    }

    private async Task<List<CodeFileSystemEntryResponse>> MapToResponseListAsync(
        List<CodeFileSystemEntry> entries,
        CancellationToken cancellationToken
    )
    {
        if (entries.Count == 0)
        {
            return [];
        }

        var directories = entries.Where(x => x.IsDirectory).ToList();
        var readmeMap = await ResolveReadmeContentMapAsync(directories, cancellationToken);

        return entries
            .Select(entry =>
            {
                string? readmeContent = null;
                if (
                    entry.IsDirectory
                    && readmeMap.TryGetValue(BuildPathKey(entry.PathSegments), out var content)
                )
                {
                    readmeContent = content;
                }

                return MapToResponse(entry, readmeContent);
            })
            .ToList();
    }

    /// <summary>
    /// 解析文件语言,目录不参与语言识别。
    /// </summary>
    /// <param name="isDirectory">是否为目录。</param>
    /// <param name="extension">文件扩展名。</param>
    /// <param name="name">文件名。</param>
    /// <returns>配置中的语言标识,目录或未配置时返回 null。</returns>
    private string? ResolveCodeLanguage(bool isDirectory, string? extension, string? name)
    {
        if (isDirectory)
        {
            return null;
        }

        return ResolveCodeLanguage(_codeViewCfg.Value, extension, name);
    }

    /// <summary>
    /// 按配置解析文件语言;未配置的扩展名或文件名不支持预览。
    /// </summary>
    /// <param name="codeView">代码查看配置。</param>
    /// <param name="extension">文件扩展名。</param>
    /// <param name="name">文件名。</param>
    /// <returns>配置中的语言标识,未匹配则返回 null。</returns>
    private static string? ResolveCodeLanguage(
        CodeViewOption codeView,
        string? extension,
        string? name
    )
    {
        foreach (var key in GetLanguageLookupKeys(extension, name))
        {
            if (
                TryGetConfiguredLanguage(
                    codeView.ExtensionToLanguage,
                    key,
                    out var configuredLanguage
                )
            )
            {
                return configuredLanguage;
            }
        }

        return null;
    }

    /// <summary>
    /// 从配置中查找语言映射,扩展名和文件名匹配忽略大小写。
    /// </summary>
    /// <param name="extensionToLanguage">扩展名和语言映射配置。</param>
    /// <param name="key">待匹配的扩展名或文件名。</param>
    /// <param name="language">匹配到的语言标识。</param>
    /// <returns>是否匹配到配置。</returns>
    private static bool TryGetConfiguredLanguage(
        Dictionary<string, string>? extensionToLanguage,
        string key,
        out string? language
    )
    {
        language = null;
        if (extensionToLanguage == null)
        {
            return false;
        }

        if (extensionToLanguage.TryGetValue(key, out var exactLanguage))
        {
            language = exactLanguage;
            return true;
        }

        var matched = extensionToLanguage.FirstOrDefault(x =>
            string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)
        );
        if (matched.Key == null)
        {
            return false;
        }

        language = matched.Value;
        return true;
    }

    /// <summary>
    /// 生成配置查找键,兼容扩展名和无扩展名的特殊文件名配置。
    /// </summary>
    /// <param name="extension">文件扩展名。</param>
    /// <param name="name">文件名。</param>
    /// <returns>用于匹配 ExtensionToLanguage 的候选键。</returns>
    private static IEnumerable<string> GetLanguageLookupKeys(string? extension, string? name)
    {
        var primary = string.IsNullOrWhiteSpace(extension) ? name : extension;
        foreach (var key in ExpandLanguageLookupKey(primary))
        {
            yield return key;
        }

        if (
            !string.IsNullOrWhiteSpace(name)
            && !string.Equals(name, primary, StringComparison.OrdinalIgnoreCase)
        )
        {
            foreach (var key in ExpandLanguageLookupKey(name))
            {
                yield return key;
            }
        }
    }

    /// <summary>
    /// 生成带点和不带点两种形式的配置键。
    /// </summary>
    /// <param name="value">扩展名或文件名。</param>
    /// <returns>规范化后的候选键。</returns>
    private static IEnumerable<string> ExpandLanguageLookupKey(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            yield break;
        }

        var normalized = value.Trim().ToLowerInvariant();
        yield return normalized;

        if (normalized.StartsWith('.'))
        {
            yield return normalized.TrimStart('.');
        }
        else
        {
            yield return "." + normalized;
        }
    }

    /// <summary>
    /// 自愈可预览文件的历史记录内容。
    /// </summary>
    /// <param name="entry">数据库中的文件系统项。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>补齐内容后的文件系统项。</returns>
    private async Task<CodeFileSystemEntry> FixPreviewContentAsync(
        CodeFileSystemEntry entry,
        CancellationToken cancellationToken
    )
    {
        var codeView = _codeViewCfg.Value;
        var shouldPreview = ResolveCodeLanguage(codeView, entry.Extension, entry.Name) != null;
        if (!shouldPreview)
        {
            return entry;
        }

        if (entry.CodeFileContentType == CodeFileContentType.Text && entry.FileContent != null)
        {
            return entry;
        }

        var fileInfo = ResolveSourceFileInfo(codeView, entry.PathSegments);
        if (fileInfo is not { Exists: true })
        {
            return entry;
        }

        entry.CodeFileContentType = CodeFileContentType.Text;
        entry.FileContent = await ReadTextContentAsync(fileInfo, cancellationToken) ?? "";
        entry.LastUpdateTime = DateTime.Now;
        await repository.UpdateAsync(entry, cancellationToken);
        return entry;
    }

    /// <summary>
    /// 根据路径分段解析源代码目录下的物理文件。
    /// </summary>
    /// <param name="codeView">代码查看配置。</param>
    /// <param name="pathSegments">相对源码根目录的路径分段。</param>
    /// <returns>位于源码根目录下的文件信息;路径越界时返回 null。</returns>
    private static FileInfo? ResolveSourceFileInfo(
        CodeViewOption codeView,
        IReadOnlyCollection<string> pathSegments
    )
    {
        if (
            string.IsNullOrWhiteSpace(codeView.SourceCodeRoot)
            || !Directory.Exists(codeView.SourceCodeRoot)
        )
        {
            throw new InvalidConfigurationException("CodeView.SourceCodeRoot 配置无效");
        }

        var sourceRoot = Path.GetFullPath(codeView.SourceCodeRoot);
        var fullPath = pathSegments.Aggregate(sourceRoot, Path.Combine);
        var normalizedFullPath = Path.GetFullPath(fullPath);
        var rootWithSeparator =
            sourceRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
            + Path.DirectorySeparatorChar;

        if (
            !string.Equals(normalizedFullPath, sourceRoot, StringComparison.OrdinalIgnoreCase)
            && !normalizedFullPath.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase)
        )
        {
            return null;
        }

        return new FileInfo(normalizedFullPath);
    }

    private async Task<Dictionary<string, string?>> ResolveReadmeContentMapAsync(
        List<CodeFileSystemEntry> directories,
        CancellationToken cancellationToken
    )
    {
        if (directories.Count == 0)
        {
            return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
        }

        var parentSegmentsList = directories.Select(x => x.PathSegments).ToList();
        var filter = Builders<CodeFileSystemEntry>.Filter.And(
            Builders<CodeFileSystemEntry>.Filter.In(x => x.ParentPathSegments, parentSegmentsList),
            Builders<CodeFileSystemEntry>.Filter.Regex(
                x => x.Name,
                new BsonRegularExpression("^readme\\.md$", "i")
            )
        );

        var readmes = await repository.SearchFor(filter).ToListAsync(cancellationToken);
        return readmes.ToDictionary(
            x => BuildPathKey(x.ParentPathSegments),
            x => x.FileContent,
            StringComparer.OrdinalIgnoreCase
        );
    }

    private async Task<string?> ResolveReadmeContentAsync(
        List<string> parentPathSegments,
        CancellationToken cancellationToken
    )
    {
        var filter = Builders<CodeFileSystemEntry>.Filter.And(
            Builders<CodeFileSystemEntry>.Filter.Eq(x => x.ParentPathSegments, parentPathSegments),
            Builders<CodeFileSystemEntry>.Filter.Regex(
                x => x.Name,
                new BsonRegularExpression("^readme\\.md$", "i")
            )
        );

        var readme = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
        return readme?.FileContent;
    }

    private static string BuildPathKey(IEnumerable<string> segments)
    {
        return string.Join("/", segments);
    }

    /// <summary>
    /// 读取文本文件内容
    /// </summary>
    private async Task<string?> ReadTextContentAsync(
        FileInfo fileInfo,
        CancellationToken cancellationToken
    )
    {
        try
        {
            using var reader = fileInfo.OpenText();
            return await reader.ReadToEndAsync(cancellationToken);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "读取文件内容失败: {FilePath}", fileInfo.FullName);
            return null;
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

通过对源码、接口、实体及相关调用方的交叉阅读,下面是对 CodeFileSystemEntryService.cs 的完整解读。

一句话定位

它是「代码文件浏览器/代码仓库管理」业务的服务层实现:把磁盘上的源码树(由 SyncCodeFileSystemEntriesRequest.cs 同步进 MongoDB 的 CodeFileSystemEntry 集合)对外提供浏览、搜索、分页、描述编辑、AI 分析判断等能力,并全程用 FusionCache 做 7 天缓存。

依赖与协作关系

  • 数据实体 CodeFileSystemEntry.cs:每条记录代表一个目录或文件,核心字段是 PathSegments(完整路径分段)、ParentPathSegmentsIsDirectoryFileContent(已入库的文本内容)、Hash、AI 分析字段等;它声明了 PathSegments / ParentPathSegments / Name / IsDirectory 相关索引。
  • 接口 ICodeFileSystemEntryService.cs:本类实现的全部公共方法与之对应。
  • 通用仓库 IRepository<CodeFileSystemEntry> + MongoDB 驱动(BuildersBsonRegularExpressionToPagedListAsync 等)。
  • 配置 CodeViewOption(见 CodeViewOption.cs):SourceCodeRoot(源码根目录)与 ExtensionToLanguage(扩展名→语言映射),由 _codeViewCfg 惰性加载,配置无效直接抛 InvalidConfigurationException
  • IFusionCache:所有查询类方法都先查缓存,键前缀/标签来自源码生成器生成的 GeneratedCodeFileSystemEntryServiceCacheMetadata(该类型是 Dpz.Core.SourceGenerator 增量生成器产物,源码树中搜索不到其定义;同步处理程序里也用 "Dpz.Core.Service.RepositoryServiceImpl.CodeFileSystemEntryService" 作为同一套缓存命名空间前缀)。
  • 静态工具:
    • CodeContentSearch.cs:内容搜索的“查询规范化/转义/片段截取”逻辑(转义避免 Mongo 正则炸弹、上限 20 条、命中片段半径 80)。
    • ApplicationTools.csWildcardToRegexPattern:把 */? 转成 glob 式正则。
  • 主要调用方:MVC 与 API 的 CodeController.cs(另一份在 src/Dpz.Core.Web/Controllers/CodeController.cs)、CodeNoteTreeExtensions.cs(构造树形浏览)、CodeExplorerMcpTools.cs(MCP 工具)、以及 WebJobs 的 AnalyzeCodeHandler.cs(写回 AI 分析结果)。

方法按职责分组讲解

1. 路径/实体浏览(读 + 缓存)

  • FindByPathAsync:规范化空分段 → 拼缓存键 → 走 GetOrSetCacheAsync
  • FindByPathWithoutCacheAsync:同一核心逻辑但绕过缓存(用于需要最新状态的场景)。
  • 核心 FindByPathCoreAsync 的查找策略是两段式
    1. 先做精确匹配PathSegments == segments,区分大小写);
    2. 未命中且分段非空时,退化为不区分大小写的正则匹配:对每个分段生成 ^转义串$ + i 选项,构造 { $size: n, $all: [...] } 的文档过滤器,保证顺序和数量都匹配;DEBUG 下把渲染出的 BSON 查询打日志便于排查。
    3. 若实体是文件,先调 FixPreviewContentAsync 尝试“自愈”内容;若是目录,再尝试解析该目录下的 README.md(忽略大小写)作为 ReadmeContent
  • GetChildrenAsync:按 ParentPathSegments == segments 取子项;返回前批量解析各子目录的 README 映射(ResolveReadmeContentMapAsync:一条 $in + ^readme\.md$ 的查询),避免 N+1。

2. 搜索

  • SearchAsync(按文件名):由 BuildSearchPattern 处理关键字——含 */? 时整体锚定首尾(glob 语义:整名匹配),否则子串匹配;走缓存。
  • SearchContentAsync(按文件内容,不缓存):仅查 IsDirectory=false && CodeFileContentType==Text,对 FileContent字面量转义后的不区分大小写正则(借助 CodeContentSearch.cs 防止用户输入成为正则炸弹),Limit 上限 20,再截取关键词周边片段组装 CodeContentSearchHitResponse

3. 管理/后台

  • GetPagedListAsync:用 [CodeFlatRequest](GenerateConditionFilter.GenerateConditionExpressionTree 动态表达式树)生成过滤条件,按 SortField(Size/CreatedTime/LastWriteTime/LastUpdateTime/AiAnalyzeTime)+ 升降序排序后分页;随后逐条补算 CodeLanguage(列表视图不返回正文)。
  • GetAllDirectoriesAsync:查所有目录(或指定前缀下的目录),在内存里用 SequenceEqual 前缀过滤后按“深度→路径字符串”排序返回,供前端下拉/树选择。

4. 写操作与缓存失效

  • SaveDescriptionAsync / SaveAiAnalyzeResultAsync:参数语义一致——path父路径name 是自身名,拼接成完整 PathSegments 后定位实体并 UpdateAsync;随后做两级缓存清理:InvalidateEntryCachesAsync(清掉该条目 FindByPathAsync 缓存 + 父目录 GetChildrenAsync 缓存)和 InvalidateSearchCacheAsync(按 name 清文件名搜索缓存)。
  • ShouldAnalyzeAsync:AI 分析触发决策。条件链:全局开关 CodeUseAIAnalyze → 代码非空 → 行数 ≥ 50 → 文件名不含 .min → 语言属于 csharp/javascript/typescript → 库里该条目存在;最后判断:无历史结果、或 AiAnalyzeHash != 当前 Hash 时返回 true(内容已变需重分析),否则 false。注意此处 path 语义与“保存类”方法不同:直接拿 path(调用方传入的是含自身文件名的完整路径)作为 PathSegments 查询,未再拼接 name

5. 缓存基础设施(私有)

  • GetOrSetCacheAsync<T>:统一封装 FusionCache 的 GetOrSetAsync,缓存时长与 fail-safe 均为 7 天;写入时附加 GeneratedCodeFileSystemEntryServiceCacheMetadata 提供的默认前缀和方法级 Tag。
  • BuildCacheKey前缀:方法名:参数,例如 ...:FindByPathAsync:Path=a/b

6. 语言识别

  • ResolveCodeLanguage 系列:目录不识别;文件按“扩展名(或文件名)→配置语言”映射。键会同时生成“带点/不带点”两种形式(cs.cs),匹配忽略大小写、先精确后遍历。

7. 内容自愈(对磁盘的兜底读取)

  • FixPreviewContentAsync:当某文件按配置应可预览,但库里 FileContent 为空或内容类型不是 Text(历史旧数据常见)时,回到 CodeView.SourceCodeRoot 下读物理文件并回写数据库。这是对“入库数据不全”的兜底。
  • ResolveSourceFileInfo:把 PathSegments 逐个 Path.Combine 拼到源码根下,并用 Path.GetFullPath + 前缀比较做目录穿越防护(拼接结果不得跳出根目录)。
  • ReadTextContentAsync:读文本失败只记警告并返回 null,不影响主流程。

8. README 解析

  • ResolveReadmeContentAsync / ResolveReadmeContentMapAsync:按“父路径 + ^readme\.md$(i)”查 README 内容,前者单查(详情页目录),后者批量 $in 查(子项列表),用于浏览目录时直接展示说明。

端到端调用流程

浏览路径时:Controller → BuildCodeNoteTreeAsyncCodeNoteTreeExtensions.cs)→ FindByPathAsync + GetChildrenAsync;当判定是文件且 ShouldAnalyzeAsync=true 时,Controller 发布 AnalyzeCodeMessage,Jobs 的 AnalyzeCodeHandler.cs 分析完成后回调 SaveAiAnalyzeResultAsync 并触发缓存清理。数据源头则由 Mediator 的 Sync 处理器扫描磁盘写入/更新/删除集合记录。

flowchart TD
    subgraph 数据入口
        Sync[SyncCodeFileSystemEntriesHandler 扫描磁盘] -->|写入/更新/删除| Mongo[(MongoDB CodeFileSystemEntry)]
    end

    subgraph 读路径-浏览与搜索
        C1[Web/WebApi CodeController / MCP工具 / CodeNoteTreeExtensions]
        C1 -->|FindByPathAsync / GetChildrenAsync / SearchAsync / GetAllDirectoriesAsync| G1{GetOrSetCacheAsync<br/>FusionCache 命中?}
        G1 -->|命中| R1[直接返回缓存]
        G1 -->|未命中| DB1[按 Mongo 过滤查询<br/>FindByPathCore 先精确后不区分大小写正则]
        DB1 --> D1{找到条目?}
        D1 -->|否| NullRet[返回 null]
        D1 -->|是|
评论加载中...