using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.Tools.FileRW;

public sealed class FileRWTool : IAgentTool
{
    /// <summary>
    /// Per-Datei Locking: Verhindert Race Conditions wenn mehrere Agenten
    /// gleichzeitig auf dieselbe Datei zugreifen (read/write/append/delete/copy).
    /// Key = normalisierter absoluter Pfad (lowercase), Value = SemaphoreSlim(1,1).
    /// </summary>
    private static readonly ConcurrentDictionary<string, SemaphoreSlim> FileLocks = new();

    private static SemaphoreSlim GetFileLock(string fullPath)
        => FileLocks.GetOrAdd(NormalizePath(fullPath), _ => new SemaphoreSlim(1, 1));

    private static string NormalizePath(string path)
        => Path.GetFullPath(path).ToLowerInvariant();

    public string Name => "FileRW";

    public string Description => "Dateiverwaltung in zwei getrennten Workspace-Verzeichnissen:\n" +
        "- workspace='personal': Dein PRIVATER Ordner — nur du hast Zugriff.\n" +
        "- workspace='shared': GETEILTER Ordner — alle Agenten im Team können darauf zugreifen.\n" +
        "Aktionen: read, write, append, list, delete, copy, stock_add.\n" +
        "Bei copy kann zwischen Workspaces kopiert werden.\n" +
        "'stock_add' fügt einen neuen Datenpunkt zur Aktien-Wissensdatenbank hinzu (shared:stocks/{ticker}/). " +
        "Geschützte Pfade (protectedPaths) erlauben nur das Erstellen neuer Dateien — kein Überschreiben oder Löschen.";

    public JsonElement InputSchema { get; } = JsonDocument.Parse("""
        {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["read", "write", "append", "list", "delete", "copy", "stock_add"],
                    "description": "Die auszuführende Aktion. 'stock_add' fügt einen strukturierten Datenpunkt zur Aktien-Wissensdatenbank hinzu."
                },
                "workspace": {
                    "type": "string",
                    "enum": ["personal", "shared"],
                    "description": "In welchem Workspace die Aktion ausgeführt werden soll (Quell-Workspace bei copy). Standard ist 'personal'."
                },
                "path": {
                    "type": "string",
                    "description": "Relativer Pfad zur Datei oder zum Verzeichnis"
                },
                "content": {
                    "type": "string",
                    "description": "Inhalt für write oder append Aktionen"
                },
                "destinationWorkspace": {
                    "type": "string",
                    "enum": ["personal", "shared"],
                    "description": "Ziel-Workspace für copy. Standard ist gleicher Workspace wie 'workspace'."
                },
                "destinationPath": {
                    "type": "string",
                    "description": "Relativer Zielpfad für copy. Pflichtfeld bei copy."
                },
                "ticker": {
                    "type": "string",
                    "description": "Aktien-Ticker für stock_add (z.B. 'NVDA', 'TSLA'). Wird automatisch zu Großbuchstaben."
                },
                "category": {
                    "type": "string",
                    "enum": ["news", "social", "analysis", "capitol", "price", "earnings", "filing", "sentiment", "other"],
                    "description": "Kategorie des Datenpunkts für stock_add."
                },
                "source": {
                    "type": "string",
                    "description": "Quelle des Datenpunkts für stock_add (z.B. 'reuters', 'x_unusual_whales', 'youtube', 'reddit')."
                },
                "title": {
                    "type": "string",
                    "description": "Kurze Überschrift/Zusammenfassung des Datenpunkts für stock_add."
                },
                "data": {
                    "type": "object",
                    "description": "Strukturierte Daten des Datenpunkts für stock_add. Kann beliebige Felder enthalten."
                }
            },
            "required": ["action"]
        }
        """).RootElement.Clone();

    public async Task<ToolResult> ExecuteAsync(
        JsonElement input,
        AgentToolContext context,
        CancellationToken ct)
    {
        var action = input.GetProperty("action").GetString()
            ?? throw new ArgumentException("'action' is required");

        var workspace = (input.TryGetProperty("workspace", out var w) ? w.GetString() : null) ?? "personal";

        var rootPath = workspace == "shared" ? context.SharedWorkspacePath : context.WorkspacePath;

        if (string.IsNullOrWhiteSpace(rootPath))
        {
            var wsName = workspace == "shared" ? "SharedWorkspace" : "PersonalWorkspace";
            return ToolResult.Fail($"Konfigurationsfehler: '{wsName}' ist nicht definiert.");
        }

        // Sicherstellen, dass rootPath absolut ist
        rootPath = Path.GetFullPath(rootPath);

        // Zugriffsprüfung
        if (!IsActionAllowed(action, workspace, context))
        {
            return ToolResult.Fail($"Zugriff verweigert: Die Aktion '{action}' ist im Workspace '{workspace}' für diesen Agenten nicht erlaubt.");
        }

        try
        {
            return action switch
            {
                "read" => await HandleReadAsync(input, rootPath, workspace, context, ct),
                "write" => await HandleWriteAsync(input, rootPath, workspace, context, ct),
                "append" => await HandleAppendAsync(input, rootPath, workspace, context, ct),
                "list" => await HandleListAsync(input, rootPath, workspace, context, ct),
                "delete" => await HandleDeleteAsync(input, rootPath, workspace, context, ct),
                "copy" => await HandleCopyAsync(input, rootPath, workspace, context, ct),
                "stock_add" => await HandleStockAddAsync(input, context, ct),
                _ => ToolResult.Fail($"Unbekannte Aktion: {action}")
            };
        }
        catch (Exception ex)
        {
            context.Logger.LogError(ex, "Fehler bei FileRW Aktion {Action}", action);
            return ToolResult.Fail($"Fehler: {ex.Message}");
        }
    }

    private bool IsActionAllowed(string action, string workspace, AgentToolContext context)
    {
        if (workspace == "personal") return true; // Alles erlaubt

        var level = context.ToolConfig.TryGetValue("sharedAccessLevel", out var lv) ? lv?.ToString() : "Denied";

        if (level == "Denied") return false;

        return action switch
        {
            "read" or "list" or "copy" => true,
            "write" or "append" or "stock_add" => level is "ReadWrite" or "Admin",
            "delete" => level is "Admin",
            _ => false
        };
    }

    /// <summary>
    /// Prüft ob ein Pfad innerhalb eines geschützten Bereichs liegt.
    /// Geschützte Pfade erlauben nur: read, list, append, write (nur neue Dateien), stock_add.
    /// Kein Überschreiben existierender Dateien, kein Löschen.
    /// Admin-Level umgeht den Schutz.
    /// </summary>
    private bool IsPathProtected(string fullPath, string rootPath, AgentToolContext context)
    {
        var protectedPaths = GetProtectedPaths(context);
        if (protectedPaths.Count == 0) return false;

        var relativePath = Path.GetRelativePath(rootPath, fullPath)
            .Replace('\\', '/').TrimStart('/');

        // GetProtectedPaths hängt an jeden Eintrag ein "/" an, deshalb greift
        // StartsWith hier auf einer echten Verzeichnisgrenze: "stocks/" trifft
        // "stocks/x.json", aber nicht "stocks-alt/x.json".
        return protectedPaths.Any(pp =>
            relativePath.StartsWith(pp, StringComparison.OrdinalIgnoreCase) ||
            relativePath.Equals(pp.TrimEnd('/'), StringComparison.OrdinalIgnoreCase));
    }

    private bool IsAdminLevel(AgentToolContext context)
    {
        var level = context.ToolConfig.TryGetValue("sharedAccessLevel", out var lv) ? lv?.ToString() : "Denied";
        return level == "Admin";
    }

    private static List<string> GetProtectedPaths(AgentToolContext context)
    {
        if (!context.ToolConfig.TryGetValue("protectedPaths", out var pp) || pp is null)
            return [];

        if (pp is JsonElement je && je.ValueKind == JsonValueKind.Array)
            return je.EnumerateArray()
                .Select(e => e.GetString()?.Replace('\\', '/').Trim().TrimEnd('/') + "/")
                .Where(s => !string.IsNullOrEmpty(s))
                .ToList()!;

        if (pp is string s && !string.IsNullOrWhiteSpace(s))
            return s.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .Select(p => p.Replace('\\', '/').TrimEnd('/') + "/")
                .ToList();

        return [];
    }

    private string GetAndValidatePath(JsonElement input, string rootPath, string workspace, bool checkExtension, AgentToolContext context)
    {
        var relativePath = input.TryGetProperty("path", out var p) ? p.GetString() : "";

        // Prüft Ausbruch aus dem Workspace, absolute Pfade und Alternate Data Streams.
        var fullPath = WorkspacePath.Resolve(rootPath, relativePath, workspace);

        // Extension Check
        if (checkExtension && !Directory.Exists(fullPath))
        {
            var extension = Path.GetExtension(fullPath).ToLowerInvariant();
            
            var configKey = workspace == "shared" ? "sharedAllowedExtensions" : "personalAllowedExtensions";
            
            var allowedExtensions = context.ToolConfig.TryGetValue(configKey, out var ae) && ae is JsonElement je
                ? je.EnumerateArray().Select(x => x.GetString()?.ToLowerInvariant()).ToList()
                : new List<string?> { ".txt", ".json", ".md", ".html", ".js", ".css" }; // Default

            if (!allowedExtensions.Contains(extension))
            {
                throw new UnauthorizedAccessException($"Dateiendung '{extension}' ist im Workspace '{workspace}' nicht erlaubt.");
            }
        }

        return fullPath;
    }

    private async Task<ToolResult> HandleReadAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
    {
        var path = GetAndValidatePath(input, rootPath, workspace, true, context);
        if (!File.Exists(path)) return ToolResult.Fail($"Datei nicht gefunden im {workspace} Workspace: {Path.GetRelativePath(rootPath, path)}");

        var fileLock = GetFileLock(path);
        await fileLock.WaitAsync(ct);
        try
        {
            var content = await File.ReadAllTextAsync(path, ct);
            var relativePath = Path.GetRelativePath(rootPath, path);
            return ToolResult.Ok($"[{workspace}:/{relativePath}]\n{content}");
        }
        finally
        {
            fileLock.Release();
        }
    }

    private async Task<ToolResult> HandleWriteAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
    {
        var path = GetAndValidatePath(input, rootPath, workspace, true, context);
        var content = input.TryGetProperty("content", out var c) ? c.GetString() ?? "" : "";

        var fileLock = GetFileLock(path);
        await fileLock.WaitAsync(ct);
        try
        {
            // Schutz: In geschützten Bereichen darf nur geschrieben werden wenn die Datei NICHT existiert
            if (workspace == "shared" && !IsAdminLevel(context) && File.Exists(path) && IsPathProtected(path, rootPath, context))
            {
                var relPath = Path.GetRelativePath(rootPath, path);
                return ToolResult.Fail(
                    $"🛡️ Geschützter Bereich: Die Datei '{relPath}' existiert bereits und darf nicht überschrieben werden. " +
                    $"Verwende 'append' um Daten hinzuzufügen, oder erstelle eine neue Datei mit anderem Namen. " +
                    $"Tipp: Nutze 'stock_add' für strukturierte Datenpunkte.");
            }

            var dir = Path.GetDirectoryName(path);
            if (dir != null && !Directory.Exists(dir)) Directory.CreateDirectory(dir);

            await File.WriteAllTextAsync(path, content, new UTF8Encoding(false), ct);
            return ToolResult.Ok($"[{workspace} Workspace] Datei erfolgreich geschrieben: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
        }
        finally
        {
            fileLock.Release();
        }
    }

    private async Task<ToolResult> HandleAppendAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
    {
        var path = GetAndValidatePath(input, rootPath, workspace, true, context);
        var content = input.TryGetProperty("content", out var c) ? c.GetString() ?? "" : "";

        var fileLock = GetFileLock(path);
        await fileLock.WaitAsync(ct);
        try
        {
            await File.AppendAllTextAsync(path, content, new UTF8Encoding(false), ct);
            return ToolResult.Ok($"[{workspace} Workspace] Inhalt erfolgreich angehängt an: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
        }
        finally
        {
            fileLock.Release();
        }
    }

    private async Task<ToolResult> HandleListAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
    {
        var path = GetAndValidatePath(input, rootPath, workspace, false, context);
        if (!Directory.Exists(path)) return ToolResult.Fail($"Verzeichnis nicht gefunden im {workspace} Workspace.");

        var relativeDirPath = Path.GetRelativePath(rootPath, path);
        var entries = Directory.GetFileSystemEntries(path)
            .Select(e => new
            {
                Name = Path.GetFileName(e),
                Type = Directory.Exists(e) ? "directory" : "file",
                Size = Directory.Exists(e) ? 0 : new FileInfo(e).Length,
                LastModified = File.GetLastWriteTime(e)
            })
            .ToList();

        var header = $"[{workspace} Workspace] Verzeichnis: {workspace}:/{(relativeDirPath == "." ? "" : relativeDirPath)}\n";
        return ToolResult.Ok(header + JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true }));
    }

    private async Task<ToolResult> HandleDeleteAsync(JsonElement input, string rootPath, string workspace, AgentToolContext context, CancellationToken ct)
    {
        var path = GetAndValidatePath(input, rootPath, workspace, false, context);

        // Schutz: In geschützten Bereichen ist Löschen komplett verboten (außer Admin)
        if (workspace == "shared" && !IsAdminLevel(context) && IsPathProtected(path, rootPath, context))
        {
            var relPath = Path.GetRelativePath(rootPath, path);
            return ToolResult.Fail(
                $"🛡️ Geschützter Bereich: '{relPath}' liegt in einem geschützten Verzeichnis und darf nicht gelöscht werden. " +
                $"Daten in geschützten Bereichen sind append-only — sie können nur ergänzt, nicht entfernt werden.");
        }

        var fileLock = GetFileLock(path);
        await fileLock.WaitAsync(ct);
        try
        {
            if (File.Exists(path))
            {
                File.Delete(path);
                return ToolResult.Ok($"[{workspace} Workspace] Datei gelöscht: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
            }
            else if (Directory.Exists(path))
            {
                Directory.Delete(path, true);
                return ToolResult.Ok($"[{workspace} Workspace] Verzeichnis gelöscht: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
            }

            return ToolResult.Fail($"Datei oder Verzeichnis nicht gefunden im {workspace} Workspace.");
        }
        finally
        {
            fileLock.Release();
        }
    }

    private async Task<ToolResult> HandleCopyAsync(JsonElement input, string sourceRootPath, string sourceWorkspace, AgentToolContext context, CancellationToken ct)
    {
        var sourcePath = GetAndValidatePath(input, sourceRootPath, sourceWorkspace, false, context);

        var destWorkspace = (input.TryGetProperty("destinationWorkspace", out var dw) ? dw.GetString() : null) ?? sourceWorkspace;
        var destRelPath = input.TryGetProperty("destinationPath", out var dp) ? dp.GetString() : null;

        if (string.IsNullOrWhiteSpace(destRelPath))
            return ToolResult.Fail("'destinationPath' ist für die copy-Aktion erforderlich.");

        var destRootPath = destWorkspace == "shared" ? context.SharedWorkspacePath : context.WorkspacePath;
        if (string.IsNullOrWhiteSpace(destRootPath))
            return ToolResult.Fail($"Konfigurationsfehler: '{(destWorkspace == "shared" ? "SharedWorkspace" : "PersonalWorkspace")}' ist nicht definiert.");

        destRootPath = Path.GetFullPath(destRootPath);

        if (!IsActionAllowed("write", destWorkspace, context))
            return ToolResult.Fail($"Zugriff verweigert: Schreiben im Workspace '{destWorkspace}' ist für diesen Agenten nicht erlaubt.");

        var destInput = JsonDocument.Parse(JsonSerializer.Serialize(new { path = destRelPath })).RootElement;
        var destPath = GetAndValidatePath(destInput, destRootPath, destWorkspace, false, context);

        if (File.Exists(sourcePath))
        {
            // Schutz: Kein Überschreiben in geschützten Bereichen per Copy
            if (destWorkspace == "shared" && !IsAdminLevel(context) && File.Exists(destPath) && IsPathProtected(destPath, destRootPath, context))
            {
                var relPath = Path.GetRelativePath(destRootPath, destPath);
                return ToolResult.Fail(
                    $"🛡️ Geschützter Bereich: Die Zieldatei '{relPath}' existiert bereits und darf nicht überschrieben werden.");
            }

            // Deadlock-sicheres Locking: Immer in alphabetischer Reihenfolge locken
            var srcNorm = NormalizePath(sourcePath);
            var dstNorm = NormalizePath(destPath);
            var first = string.Compare(srcNorm, dstNorm, StringComparison.Ordinal) <= 0
                ? GetFileLock(sourcePath) : GetFileLock(destPath);
            var second = string.Compare(srcNorm, dstNorm, StringComparison.Ordinal) <= 0
                ? GetFileLock(destPath) : GetFileLock(sourcePath);

            await first.WaitAsync(ct);
            try
            {
                await second.WaitAsync(ct);
                try
                {
                    var destDir = Path.GetDirectoryName(destPath);
                    if (destDir != null && !Directory.Exists(destDir)) Directory.CreateDirectory(destDir);

                    await Task.Run(() => File.Copy(sourcePath, destPath, overwrite: !IsPathProtected(destPath, destRootPath, context)), ct);

                    var srcLabel = $"{sourceWorkspace}:/{Path.GetRelativePath(sourceRootPath, sourcePath)}";
                    var dstLabel = $"{destWorkspace}:/{Path.GetRelativePath(destRootPath, destPath)}";
                    return ToolResult.Ok($"Datei kopiert: {srcLabel} → {dstLabel}");
                }
                finally
                {
                    second.Release();
                }
            }
            finally
            {
                first.Release();
            }
        }
        else if (Directory.Exists(sourcePath))
        {
            await Task.Run(() => CopyDirectory(sourcePath, destPath), ct);

            var srcLabel = $"{sourceWorkspace}:/{Path.GetRelativePath(sourceRootPath, sourcePath)}";
            var dstLabel = $"{destWorkspace}:/{Path.GetRelativePath(destRootPath, destPath)}";
            return ToolResult.Ok($"Verzeichnis kopiert: {srcLabel} → {dstLabel}");
        }

        return ToolResult.Fail("Quelldatei oder -verzeichnis nicht gefunden.");
    }

    // ═══════════════════════════════════════════════════
    //  STOCK DATABASE (Append-Only Wissensdatenbank)
    // ═══════════════════════════════════════════════════

    private static readonly JsonSerializerOptions StockJsonOpts = new()
    {
        WriteIndented = true,
        PropertyNameCaseInsensitive = true,
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    };

    private async Task<ToolResult> HandleStockAddAsync(JsonElement input, AgentToolContext context, CancellationToken ct)
    {
        // Pflichtfelder
        var ticker = (input.TryGetProperty("ticker", out var t) ? t.GetString() : null)?.Trim().ToUpperInvariant();
        if (string.IsNullOrWhiteSpace(ticker))
            return ToolResult.Fail("'ticker' ist ein Pflichtfeld für stock_add (z.B. 'NVDA', 'TSLA').");

        var category = (input.TryGetProperty("category", out var cat) ? cat.GetString() : null) ?? "other";
        var source = (input.TryGetProperty("source", out var src) ? src.GetString() : null) ?? "unknown";
        var title = (input.TryGetProperty("title", out var ttl) ? ttl.GetString() : null) ?? "";
        var content = input.TryGetProperty("content", out var cnt) ? cnt.GetString() : null;
        var data = input.TryGetProperty("data", out var d) ? d : (JsonElement?)null;

        if (string.IsNullOrWhiteSpace(title) && string.IsNullOrWhiteSpace(content) && data is null)
            return ToolResult.Fail("Mindestens 'title', 'content' oder 'data' muss angegeben werden.");

        // SharedWorkspace ist Pflicht für stocks
        var sharedRoot = context.SharedWorkspacePath;
        if (string.IsNullOrWhiteSpace(sharedRoot))
            return ToolResult.Fail("SharedWorkspace ist nicht konfiguriert.");

        sharedRoot = Path.GetFullPath(sharedRoot);

        // Zugriffsprüfung
        if (!IsActionAllowed("stock_add", "shared", context))
            return ToolResult.Fail("Zugriff verweigert: Schreiben im SharedWorkspace nicht erlaubt.");

        // Sanitize Ticker für Verzeichnisname
        var safeTicker = SanitizeFileName(ticker);
        var stockDir = Path.Combine(sharedRoot, "stocks", safeTicker);
        Directory.CreateDirectory(stockDir);

        // Timestamp-basierten Dateinamen generieren
        var now = DateTime.UtcNow;
        var timestamp = now.ToString("yyyyMMdd_HHmmss");
        var safeSource = SanitizeFileName(source);
        var fileName = $"{timestamp}_{category}_{safeSource}.json";
        var filePath = Path.Combine(stockDir, fileName);

        // Kollisionsvermeidung
        var counter = 1;
        while (File.Exists(filePath))
        {
            fileName = $"{timestamp}_{category}_{safeSource}_{counter}.json";
            filePath = Path.Combine(stockDir, fileName);
            counter++;
        }

        // Datenpunkt erstellen
        var entry = new Dictionary<string, object?>
        {
            ["id"] = $"{safeTicker}_{timestamp}_{category}_{safeSource}",
            ["ticker"] = ticker,
            ["category"] = category,
            ["source"] = source,
            ["title"] = title,
            ["timestamp"] = now.ToString("o"),
            ["agent"] = context.AgentId
        };

        if (!string.IsNullOrWhiteSpace(content))
            entry["content"] = content;

        if (data.HasValue)
            entry["data"] = data.Value;

        // Datenpunkt speichern
        var json = JsonSerializer.Serialize(entry, StockJsonOpts);
        await File.WriteAllTextAsync(filePath, json, new UTF8Encoding(false), ct);

        // _index.json aktualisieren (thread-safe via per-file SemaphoreSlim, append-only)
        var indexPath = Path.Combine(stockDir, "_index.json");
        var indexEntry = new Dictionary<string, object?>
        {
            ["file"] = fileName,
            ["category"] = category,
            ["source"] = source,
            ["title"] = title,
            ["timestamp"] = now.ToString("o"),
            ["agent"] = context.AgentId
        };

        var indexLock = GetFileLock(indexPath);
        await indexLock.WaitAsync(ct);
        try
        {
            List<Dictionary<string, object?>> index;
            if (File.Exists(indexPath))
            {
                try
                {
                    var existing = await File.ReadAllTextAsync(indexPath, ct);
                    index = JsonSerializer.Deserialize<List<Dictionary<string, object?>>>(existing, StockJsonOpts)
                        ?? [];
                }
                catch (JsonException)
                {
                    // Korrupter Index: Backup + Neustart
                    var backup = indexPath + $".bak_{now:yyyyMMdd_HHmmss}";
                    try { File.Copy(indexPath, backup, overwrite: true); } catch { /* best effort */ }
                    index = [];
                }
            }
            else
            {
                index = [];
            }

            index.Add(indexEntry);
            // Atomar: Der Index ist die einzige Übersicht über die Datenpunkte —
            // eine halb geschriebene Datei wäre nicht wiederherstellbar.
            await ClawdDotNet.Core.Storage.AtomicFile.WriteAllTextAsync(
                indexPath, JsonSerializer.Serialize(index, StockJsonOpts), ct: ct);
        }
        finally
        {
            indexLock.Release();
        }

        var relPath = $"shared:/stocks/{safeTicker}/{fileName}";
        return ToolResult.Ok(
            $"✅ Datenpunkt hinzugefügt: {relPath}\n" +
            $"Ticker: {ticker} | Kategorie: {category} | Quelle: {source}\n" +
            $"Titel: {title}\n" +
            $"Index: {index_Count(indexPath)} Einträge für {ticker}");
    }

    private static int index_Count(string indexPath)
    {
        try
        {
            var json = File.ReadAllText(indexPath);
            using var doc = JsonDocument.Parse(json);
            return doc.RootElement.GetArrayLength();
        }
        catch { return -1; }
    }

    private static string SanitizeFileName(string name)
    {
        var sanitized = name.Trim();
        foreach (var c in Path.GetInvalidFileNameChars())
            sanitized = sanitized.Replace(c, '_');
        return sanitized.Replace(' ', '_');
    }

    private static void CopyDirectory(string sourceDir, string destinationDir)
    {
        Directory.CreateDirectory(destinationDir);

        foreach (var file in Directory.GetFiles(sourceDir))
            File.Copy(file, Path.Combine(destinationDir, Path.GetFileName(file)), overwrite: true);

        foreach (var dir in Directory.GetDirectories(sourceDir))
            CopyDirectory(dir, Path.Combine(destinationDir, Path.GetFileName(dir)));
    }
}
