using System.Text;
using System.Text.Json;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tools;
using FluentFTP;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.Tools.FTP;

public sealed class FTPTool : IAgentTool
{
    public string Name => "FTP";

    public string Description => """
        Erlaubt den Dateitransfer zwischen dem lokalen Workspace und einem Remote-FTP/FTPS-Server.

        Für localPath werden die gleichen Workspace-Prefixe wie beim FileRW-Tool unterstützt:
        - "personal:website/index.html" → Datei im persönlichen Agent-Workspace
        - "shared:website/index.html" → Datei im geteilten SharedWorkspace
        - Ohne Prefix wird der persönliche Workspace verwendet.

        Beispiel Upload: { "action": "upload", "localPath": "shared:website/index.html", "remotePath": "/index.html" }
        """;

    public JsonElement InputSchema { get; } = JsonDocument.Parse("""
        {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["upload", "download", "list", "delete"],
                    "description": "Die auszuführende Aktion"
                },
                "localPath": {
                    "type": "string",
                    "description": "Pfad zur lokalen Datei mit optionalem Workspace-Prefix: 'personal:datei.txt' (Standard) oder 'shared:datei.txt' für den SharedWorkspace. Ohne Prefix wird der persönliche Workspace verwendet."
                },
                "remotePath": {
                    "type": "string",
                    "description": "Pfad auf dem FTP-Server"
                }
            },
            "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 config = context.ToolConfig;
        var host = config.TryGetValue("host", out var h) ? h?.ToString() : null;
        var username = config.TryGetValue("username", out var u) ? u?.ToString() : null;
        var password = config.TryGetValue("password", out var p) ? p?.ToString() : null;
        var port = 21;
        if (config.TryGetValue("port", out var po) && po is JsonElement portElement)
            port = portElement.TryGetInt32(out var p32) ? p32 : 21;

        if (string.IsNullOrWhiteSpace(host))
        {
            return ToolResult.Fail("Konfigurationsfehler: 'host' ist erforderlich.");
        }

        // SSL/TLS-Modus aus Config lesen (Standard: true)
        var useSsl = true;
        if (config.TryGetValue("useSsl", out var sslVal))
        {
            if (sslVal is JsonElement sslElem) useSsl = sslElem.GetBoolean();
            else if (sslVal is bool sslBool) useSsl = sslBool;
        }

        using var ftpClient = new AsyncFtpClient(host, username, password, port);

        if (useSsl)
        {
            ftpClient.Config.EncryptionMode = FtpEncryptionMode.Explicit;
            ftpClient.Config.ValidateAnyCertificate = true;
        }

        try
        {
            await ftpClient.Connect(ct);

            return action switch
            {
                "upload" => await HandleUploadAsync(ftpClient, input, context, ct),
                "download" => await HandleDownloadAsync(ftpClient, input, context, ct),
                "list" => await HandleListAsync(ftpClient, input, context, ct),
                "delete" => await HandleDeleteAsync(ftpClient, input, context, ct),
                _ => ToolResult.Fail($"Unbekannte Aktion: {action}")
            };
        }
        catch (Exception ex)
        {
            context.Logger.LogError(ex, "Fehler bei FTP Aktion {Action} auf {Host}", action, host);
            return ToolResult.Fail($"Fehler: {ex.Message}");
        }
        finally
        {
            if (ftpClient.IsConnected) await ftpClient.Disconnect(ct);
        }
    }

    /// <summary>
    /// Löst einen localPath mit optionalem Workspace-Prefix auf.
    /// "shared:website/index.html" → SharedWorkspace + "website/index.html"
    /// "personal:datei.txt" → persönlicher Workspace + "datei.txt"
    /// "datei.txt" → persönlicher Workspace + "datei.txt" (Standard)
    /// </summary>
    private (string fullPath, string workspaceLabel) ResolveLocalPath(string localPath, AgentToolContext context)
    {
        string basePath;
        string relativePath;
        string label;

        if (localPath.StartsWith("shared:", StringComparison.OrdinalIgnoreCase))
        {
            relativePath = localPath[7..]; // nach "shared:"
            basePath = context.SharedWorkspacePath
                ?? throw new ArgumentException("Kein SharedWorkspace konfiguriert.");
            label = "shared";
        }
        else if (localPath.StartsWith("personal:", StringComparison.OrdinalIgnoreCase))
        {
            relativePath = localPath[9..]; // nach "personal:"
            basePath = context.WorkspacePath
                ?? throw new ArgumentException("Kein persönlicher Workspace konfiguriert.");
            label = "personal";
        }
        else
        {
            // Kein Prefix → persönlicher Workspace als Standard
            relativePath = localPath;
            basePath = context.WorkspacePath
                ?? throw new ArgumentException("Kein persönlicher Workspace konfiguriert.");
            label = "personal";
        }

        // Config-rootPath als Override (nur wenn kein Prefix verwendet)
        if (label == "personal" && !localPath.Contains(':'))
        {
            if (context.ToolConfig.TryGetValue("rootPath", out var rp) && !string.IsNullOrWhiteSpace(rp?.ToString()))
            {
                basePath = rp.ToString()!;
            }
        }

        basePath = Path.GetFullPath(basePath);
        var fullPath = Path.GetFullPath(Path.Combine(basePath, relativePath));

        // Sicherheitsprüfung: Kein Ausbruch aus dem Workspace.
        //
        // PathBoundary statt eines Zeichenketten-Präfixes: Es vergleicht auf
        // Verzeichnisgrenzen (sonst gälte "…/Workspace-Backup" als Teil von
        // "…/Workspace"), berücksichtigt die Groß-/Kleinschreibung des Dateisystems
        // und löst symbolische Verknüpfungen auf.
        if (!PathBoundary.IsInside(fullPath, basePath))
        {
            throw new UnauthorizedAccessException(
                $"Zugriff außerhalb des {label} Workspace-Verzeichnisses verweigert.");
        }

        return (fullPath, label);
    }

    private async Task<ToolResult> HandleUploadAsync(IAsyncFtpClient client, JsonElement input, AgentToolContext context, CancellationToken ct)
    {
        var localRel = input.TryGetProperty("localPath", out var lp) ? lp.GetString() : null;
        var remote = input.TryGetProperty("remotePath", out var rp) ? rp.GetString() : null;

        if (string.IsNullOrWhiteSpace(localRel) || string.IsNullOrWhiteSpace(remote))
        {
            return ToolResult.Fail("'localPath' und 'remotePath' sind für upload erforderlich.");
        }

        try
        {
            var (localFull, wsLabel) = ResolveLocalPath(localRel, context);

            if (!File.Exists(localFull))
                return ToolResult.Fail($"Lokale Datei nicht gefunden: {localRel} (aufgelöst: {localFull})");

            var status = await client.UploadFile(localFull, remote, FtpRemoteExists.Overwrite, true, FtpVerify.None, null, ct);
            if (status != FtpStatus.Success)
                return ToolResult.Fail($"Upload fehlgeschlagen: {status}");

            // Bei HTML/CSS/JS-Uploads: .htaccess für UTF-8 sicherstellen
            var extra = "";
            if (IsWebFile(remote))
            {
                var htaccessDeployed = await EnsureHtaccessAsync(client, remote, context.Logger, ct);
                if (htaccessDeployed)
                    extra = " (.htaccess für UTF-8-Encoding wurde automatisch erstellt)";
            }

            return ToolResult.Ok($"[{wsLabel} Workspace] Datei erfolgreich hochgeladen: {localRel} → {remote}{extra}");
        }
        catch (ArgumentException ex)
        {
            return ToolResult.Fail(ex.Message);
        }
        catch (UnauthorizedAccessException ex)
        {
            return ToolResult.Fail(ex.Message);
        }
    }

    private static bool IsWebFile(string remotePath)
    {
        var ext = Path.GetExtension(remotePath).ToLowerInvariant();
        return ext is ".html" or ".htm" or ".css" or ".js" or ".json" or ".xml" or ".svg";
    }

    private const string HtaccessCharsetMarker = "# ClawdDotNet-UTF8-Charset";
    private const string HtaccessCharsetBlock = $"""

        {HtaccessCharsetMarker}
        AddDefaultCharset UTF-8
        <IfModule mod_mime.c>
            AddCharset UTF-8 .html .htm .css .js .json .xml .svg .txt
        </IfModule>
        """;

    /// <summary>
    /// Stellt sicher, dass im Zielverzeichnis eine .htaccess mit UTF-8 Charset existiert.
    /// - Existiert keine .htaccess: Neue mit nur den Charset-Regeln erstellen.
    /// - Existiert eine .htaccess: Charset-Block anhängen, falls der Marker noch nicht drin ist.
    ///   Der bestehende Inhalt (z.B. Zugriffsschutz, Rewrite-Regeln) bleibt IMMER unangetastet.
    /// </summary>
    private async Task<bool> EnsureHtaccessAsync(IAsyncFtpClient client, string remotePath, ILogger logger, CancellationToken ct)
    {
        try
        {
            var remoteDir = Path.GetDirectoryName(remotePath)?.Replace('\\', '/') ?? "/";
            if (string.IsNullOrWhiteSpace(remoteDir)) remoteDir = "/";
            var htaccessPath = remoteDir.TrimEnd('/') + "/.htaccess";

            if (await client.FileExists(htaccessPath, ct))
            {
                // .htaccess existiert — lesen und prüfen ob UTF-8-Block schon drin ist
                using var downloadStream = new MemoryStream();
                if (!await client.DownloadStream(downloadStream, htaccessPath, token: ct))
                    return false; // Download fehlgeschlagen → nichts tun

                downloadStream.Position = 0;
                var existingContent = Encoding.UTF8.GetString(downloadStream.ToArray());

                // Wenn unser Marker bereits enthalten ist → nichts tun
                if (existingContent.Contains(HtaccessCharsetMarker, StringComparison.Ordinal))
                    return false;

                // Wenn bereits AddDefaultCharset gesetzt ist → nicht einmischen
                if (existingContent.Contains("AddDefaultCharset", StringComparison.OrdinalIgnoreCase))
                    return false;

                // Charset-Block an bestehenden Inhalt anhängen
                var newContent = existingContent.TrimEnd() + "\n" + HtaccessCharsetBlock;
                var bytes = Encoding.UTF8.GetBytes(newContent);
                using var uploadStream = new MemoryStream(bytes);
                var status = await client.UploadStream(uploadStream, htaccessPath, FtpRemoteExists.Overwrite, true, null, ct);

                if (status == FtpStatus.Success)
                {
                    logger.LogInformation("UTF-8 Charset-Block an bestehende .htaccess angehängt: {Path}", htaccessPath);
                    return true;
                }
            }
            else
            {
                // Keine .htaccess vorhanden → neue erstellen (nur Charset-Regeln)
                var content = HtaccessCharsetMarker + "\nAddDefaultCharset UTF-8\n\n<IfModule mod_mime.c>\n    AddCharset UTF-8 .html .htm .css .js .json .xml .svg .txt\n</IfModule>\n";
                var bytes = Encoding.UTF8.GetBytes(content);
                using var stream = new MemoryStream(bytes);
                var status = await client.UploadStream(stream, htaccessPath, FtpRemoteExists.Skip, true, null, ct);

                if (status == FtpStatus.Success)
                {
                    logger.LogInformation("Neue UTF-8 .htaccess erstellt: {Path}", htaccessPath);
                    return true;
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Konnte .htaccess nicht automatisch anpassen (nicht kritisch)");
        }

        return false;
    }

    private async Task<ToolResult> HandleDownloadAsync(IAsyncFtpClient client, JsonElement input, AgentToolContext context, CancellationToken ct)
    {
        var localRel = input.TryGetProperty("localPath", out var lp) ? lp.GetString() : null;
        var remote = input.TryGetProperty("remotePath", out var rp) ? rp.GetString() : null;

        if (string.IsNullOrWhiteSpace(localRel) || string.IsNullOrWhiteSpace(remote))
        {
            return ToolResult.Fail("'localPath' und 'remotePath' sind für download erforderlich.");
        }

        try
        {
            var (localFull, wsLabel) = ResolveLocalPath(localRel, context);

            var localDir = Path.GetDirectoryName(localFull);
            if (localDir != null && !Directory.Exists(localDir)) Directory.CreateDirectory(localDir);

            var status = await client.DownloadFile(localFull, remote, FtpLocalExists.Overwrite, FtpVerify.None, null, ct);
            return status == FtpStatus.Success
                ? ToolResult.Ok($"[{wsLabel} Workspace] Datei erfolgreich heruntergeladen: {remote} → {localRel}")
                : ToolResult.Fail($"Download fehlgeschlagen: {status}");
        }
        catch (ArgumentException ex)
        {
            return ToolResult.Fail(ex.Message);
        }
        catch (UnauthorizedAccessException ex)
        {
            return ToolResult.Fail(ex.Message);
        }
    }

    private async Task<ToolResult> HandleListAsync(IAsyncFtpClient client, JsonElement input, AgentToolContext context, CancellationToken ct)
    {
        var remote = input.TryGetProperty("remotePath", out var rp) ? rp.GetString() : "/";
        var items = await client.GetListing(remote, FtpListOption.Recursive, ct);

        var result = items.Select(i => new
        {
            i.Name,
            i.FullName,
            Type = i.Type.ToString(),
            Size = i.Size,
            Modified = i.Modified
        }).ToList();

        return ToolResult.Ok(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
    }

    private async Task<ToolResult> HandleDeleteAsync(IAsyncFtpClient client, JsonElement input, AgentToolContext context, CancellationToken ct)
    {
        var remote = input.TryGetProperty("remotePath", out var rp) ? rp.GetString() : null;
        if (string.IsNullOrWhiteSpace(remote)) return ToolResult.Fail("'remotePath' ist für delete erforderlich.");

        await client.DeleteFile(remote, ct);
        return ToolResult.Ok($"Datei/Verzeichnis gelöscht: {remote}");
    }
}
