namespace ClaudeDo.Data;

public sealed class AttachmentStore
{
    private const long MaxBytes = 5 * 1024 * 1024; // 5 MB

    private readonly string _root;

    public AttachmentStore(string? root = null)
        => _root = root ?? Path.Combine(Paths.AppDataRoot(), "attachments");

    public string Root => _root;

    public IReadOnlyList<string> EnumerateTaskIds()
    {
        if (!Directory.Exists(_root)) return Array.Empty<string>();
        return Directory.GetDirectories(_root)
            .Select(Path.GetFileName)
            .Where(n => n is not null)
            .Select(n => n!)
            .ToList();
    }

    public string TaskDir(string taskId)
        => Path.Combine(_root, taskId);

    public async Task<long> SaveAsync(string taskId, string fileName, Stream content, CancellationToken ct = default)
    {
        if (Path.GetFileName(fileName) != fileName)
            throw new ArgumentException("fileName must not contain path separators or '..'.", nameof(fileName));

        var dir = TaskDir(taskId);
        var resolvedPath = Path.GetFullPath(Path.Combine(dir, fileName));

        // Containment guard: resolved path must stay inside TaskDir
        var resolvedDir = Path.GetFullPath(dir);
        if (!resolvedPath.StartsWith(resolvedDir + Path.DirectorySeparatorChar, StringComparison.Ordinal)
            && !resolvedPath.Equals(resolvedDir, StringComparison.Ordinal))
            throw new ArgumentException("fileName resolves outside the task directory.", nameof(fileName));

        Directory.CreateDirectory(dir);

        // Buffer up to MaxBytes + 1 to detect oversize without reading fully
        await using var fs = new FileStream(resolvedPath, FileMode.Create, FileAccess.Write, FileShare.None,
            bufferSize: 81920, useAsync: true);

        var buffer = new byte[81920];
        long total = 0;
        int read;
        while ((read = await content.ReadAsync(buffer, ct)) > 0)
        {
            total += read;
            if (total > MaxBytes)
            {
                fs.Close();
                try { File.Delete(resolvedPath); } catch { }
                throw new InvalidOperationException($"Attachment exceeds the 5 MB size limit.");
            }
            await fs.WriteAsync(buffer.AsMemory(0, read), ct);
        }

        return total;
    }

    public void DeleteFile(string taskId, string fileName)
    {
        if (Path.GetFileName(fileName) != fileName)
            return; // traversal attempt — ignore silently

        var dir = TaskDir(taskId);
        var resolvedPath = Path.GetFullPath(Path.Combine(dir, fileName));
        var resolvedDir = Path.GetFullPath(dir);
        if (!resolvedPath.StartsWith(resolvedDir + Path.DirectorySeparatorChar, StringComparison.Ordinal)
            && !resolvedPath.Equals(resolvedDir, StringComparison.Ordinal))
            return; // containment violation — ignore silently

        try { File.Delete(resolvedPath); } catch (DirectoryNotFoundException) { } catch (FileNotFoundException) { }
    }

    public void DeleteTaskDir(string taskId)
    {
        var dir = TaskDir(taskId);
        try { Directory.Delete(dir, recursive: true); } catch (DirectoryNotFoundException) { } catch (IOException) { }
    }
}
