using Nexus.Api.Models;

namespace Nexus.Api.Services;

public sealed class MemoryService(
    IOpenClawAgentConfigurationService configuration) : IMemoryService
{
    private const string MemoryDirectory = "memory";

    public async Task<IReadOnlyList<MemoryFileInfo>> GetAllAsync(
        string agentId,
        CancellationToken cancellationToken = default)
    {
        var normalizedAgentId = NormalizeAgentId(agentId);
        var result = new List<MemoryFileInfo>();
        var agentFiles = await configuration.GetAgentFilesAsync(
            normalizedAgentId,
            cancellationToken);
        var longTerm = agentFiles.Files.SingleOrDefault(file =>
            string.Equals(file.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase));
        if (longTerm is { Missing: false })
        {
            result.Add(new MemoryFileInfo(
                "MEMORY.md",
                "MEMORY.md",
                longTerm.Size ?? 0,
                (longTerm.UpdatedAt ?? agentFiles.CheckedAt).UtcDateTime,
                normalizedAgentId,
                "MEMORY.md"));
        }

        var workspace = await TryGetMemoryWorkspaceAsync(
            normalizedAgentId,
            cancellationToken);
        if (workspace is not null)
        {
            result.AddRange(workspace.Entries
                .Where(OpenClawContentReadHelpers.IsMarkdownFile)
                .OrderByDescending(
                    entry => entry.Name,
                    StringComparer.OrdinalIgnoreCase)
                .Select(entry => new MemoryFileInfo(
                    entry.Name,
                    OpenClawContentReadHelpers.LegacyPath(
                        entry.Path,
                        MemoryDirectory),
                    entry.Size ?? 0,
                    (entry.UpdatedAt ?? workspace.CheckedAt).UtcDateTime,
                    normalizedAgentId,
                    entry.Path)));
        }
        return result;
    }

    public async Task<IReadOnlyList<MemorySearchResult>> SearchAsync(
        string query,
        string agentId,
        CancellationToken cancellationToken = default)
    {
        var normalizedAgentId = NormalizeAgentId(agentId);
        var normalizedQuery = query.Trim();
        var candidates = new List<MemoryCandidate>();
        var agentFiles = await configuration.GetAgentFilesAsync(
            normalizedAgentId,
            cancellationToken);
        if (agentFiles.Files.Any(file =>
                string.Equals(file.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase)
                && !file.Missing
                && (file.Size is null
                    || file.Size <= OpenClawContentReadHelpers.MaxContentBytes)))
        {
            candidates.Add(new MemoryCandidate(
                "MEMORY.md",
                "MEMORY.md",
                "MEMORY.md",
                true,
                0));
        }

        var workspace = await TryGetMemoryWorkspaceAsync(
            normalizedAgentId,
            cancellationToken);
        if (workspace is not null)
        {
            candidates.AddRange(workspace.Entries
                .Where(OpenClawContentReadHelpers.IsMarkdownFile)
                .Select(entry => new MemoryCandidate(
                    entry.Name,
                    OpenClawContentReadHelpers.LegacyPath(
                        entry.Path,
                        MemoryDirectory),
                    entry.Path,
                    false,
                    entry.Size ?? 0)));
        }

        return await OpenClawContentReadHelpers.SelectBoundedAsync<
            MemoryCandidate,
            MemorySearchResult>(
            candidates,
            async (candidate, token) =>
            {
                string? content;
                long size;
                if (candidate.AgentFile)
                {
                    var file = await configuration.GetAgentFileAsync(
                        normalizedAgentId,
                        "MEMORY.md",
                        token);
                    content = file.Missing
                        || file.Content is null
                        || file.Size > OpenClawContentReadHelpers.MaxContentBytes
                        || System.Text.Encoding.UTF8.GetByteCount(
                            file.Content) >
                           OpenClawContentReadHelpers.MaxContentBytes
                        ? null
                        : file.Content;
                    size = file.Size ?? 0;
                }
                else
                {
                    var file = await configuration.GetWorkspaceFileAsync(
                        normalizedAgentId,
                        candidate.WorkspacePath,
                        token);
                    content = OpenClawContentReadHelpers.ReadText(file);
                    size = file.Size;
                }

                if (content is null)
                    return null;
                var index = content.IndexOf(
                    normalizedQuery,
                    StringComparison.OrdinalIgnoreCase);
                if (index < 0)
                    return null;
                var start = Math.Max(0, index - 60);
                var length = Math.Min(200, content.Length - start);
                var excerpt =
                    (start > 0 ? "…" : string.Empty)
                    + content.Substring(start, length)
                    + (start + length < content.Length ? "…" : string.Empty);
                return new MemorySearchResult(
                    candidate.Name,
                    candidate.LegacyPath,
                    excerpt,
                    size,
                    normalizedAgentId,
                    candidate.WorkspacePath);
            },
            cancellationToken);
    }

    public async Task<MemoryFileContent?> GetFileAsync(
        string name,
        string agentId,
        CancellationToken cancellationToken = default)
    {
        var normalizedAgentId = NormalizeAgentId(agentId);
        if (string.Equals(name, "MEMORY.md", StringComparison.OrdinalIgnoreCase))
        {
            var file = await configuration.GetAgentFileAsync(
                normalizedAgentId,
                "MEMORY.md",
                cancellationToken);
            if (file.Missing
                || file.Content is null
                || file.Size > OpenClawContentReadHelpers.MaxContentBytes
                || System.Text.Encoding.UTF8.GetByteCount(file.Content) >
                   OpenClawContentReadHelpers.MaxContentBytes)
                return null;
            return new MemoryFileContent(
                "MEMORY.md",
                "MEMORY.md",
                file.Content,
                file.Size ?? 0,
                (file.UpdatedAt ?? file.CheckedAt).UtcDateTime,
                normalizedAgentId,
                "MEMORY.md");
        }

        if (!OpenClawContentReadHelpers.IsSafeFileName(name))
            return null;
        var workspacePath = $"{MemoryDirectory}/{name}";
        try
        {
            var file = await configuration.GetWorkspaceFileAsync(
                normalizedAgentId,
                workspacePath,
                cancellationToken);
            var content = OpenClawContentReadHelpers.ReadText(file);
            if (content is null)
                return null;
            return new MemoryFileContent(
                file.Name,
                name,
                content,
                file.Size,
                (file.UpdatedAt ?? file.CheckedAt).UtcDateTime,
                normalizedAgentId,
                file.Path);
        }
        catch (OpenClawGatewayRpcException exception)
            when (OpenClawContentReadHelpers.IsNotFound(exception))
        {
            return null;
        }
    }

    private static string NormalizeAgentId(string? agentId)
        => string.IsNullOrWhiteSpace(agentId)
            ? "iris"
            : agentId.Trim().ToLowerInvariant();

    private async Task<OpenClawWorkspaceCollectionDto?>
        TryGetMemoryWorkspaceAsync(
            string agentId,
            CancellationToken cancellationToken)
    {
        try
        {
            return await configuration.GetWorkspaceAsync(
                agentId,
                MemoryDirectory,
                0,
                OpenClawContentReadHelpers.MaxFiles,
                cancellationToken);
        }
        catch (OpenClawGatewayRpcException exception)
            when (OpenClawContentReadHelpers.IsNotFound(exception))
        {
            return null;
        }
    }

    private sealed record MemoryCandidate(
        string Name,
        string LegacyPath,
        string WorkspacePath,
        bool AgentFile,
        long Size);
}
