using System.Data;
using System.Data.Common;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using MySqlConnector;
using Npgsql;

namespace ClawdDotNet.Tools.Database;

public enum DatabaseAccessLevel
{
    ReadOnly,
    ReadWrite,
    Admin
}

public sealed class DatabaseTool : IAgentTool
{
    public string Name => "Database";

    public string Description => "Erlaubt den Zugriff auf SQL (MySQL, Postgres, MSSQL) und NoSQL (MongoDB) Datenbanken mit verschiedenen Zugriffsebenen.";

    public JsonElement InputSchema { get; } = JsonDocument.Parse("""
        {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["query", "insert", "upsert"],
                    "description": "Die auszuführende Aktion"
                },
                "sql": {
                    "type": "string",
                    "description": "Das auszuführende SQL Statement (nur für SQL-Typen)"
                },
                "collection": {
                    "type": "string",
                    "description": "Der Name der Collection (nur für MongoDB)"
                },
                "filter": {
                    "type": "string",
                    "description": "JSON-Filter (nur für MongoDB)"
                },
                "document": {
                    "type": "string",
                    "description": "JSON-Dokument zum Einfügen/Update (nur für MongoDB)"
                }
            },
            "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 type = context.ToolConfig.TryGetValue("type", out var t) ? t?.ToString()?.ToLowerInvariant() : null;
        var connectionString = context.ToolConfig.TryGetValue("connectionString", out var cs) ? cs?.ToString() : null;

        if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(connectionString))
        {
            return ToolResult.Fail("Konfigurationsfehler: 'type' und 'connectionString' sind erforderlich.");
        }

        try
        {
            if (type == "mongodb")
            {
                return await HandleMongoAsync(action, input, connectionString, context, ct);
            }
            else
            {
                return await HandleSqlAsync(type, action, input, connectionString, context, ct);
            }
        }
        catch (Exception ex)
        {
            context.Logger.LogError(ex, "Fehler bei Database Aktion {Action} ({Type})", action, type);
            return ToolResult.Fail($"Fehler: {ex.Message}");
        }
    }

    private async Task<ToolResult> HandleSqlAsync(string type, string action, JsonElement input, string connectionString, AgentToolContext context, CancellationToken ct)
    {
        var sql = input.TryGetProperty("sql", out var s) ? s.GetString() : null;
        if (string.IsNullOrWhiteSpace(sql)) return ToolResult.Fail("'sql' ist erforderlich.");

        var accessLevel = GetAccessLevel(context);

        // Statement analysieren: Kommentare, Mehrfach-Statements und String-Literale
        // werden dabei behandelt, bevor Operation und Tabellen bestimmt werden.
        var inspection = SqlGuard.Inspect(sql, GetAllowedTables(context));

        if (!inspection.IsValid)
            return ToolResult.Fail($"Sicherheitsfehler: {inspection.Error}");

        switch (inspection.Operation)
        {
            case SqlOperation.Schema when accessLevel != DatabaseAccessLevel.Admin:
                return ToolResult.Fail(
                    "Sicherheitsfehler: Strukturänderungen (DDL) sind für diesen Agenten nicht erlaubt.");

            case SqlOperation.Write when accessLevel == DatabaseAccessLevel.ReadOnly:
                return ToolResult.Fail(
                    "Sicherheitsfehler: Schreibzugriff ist für diesen Agenten deaktiviert (ReadOnly).");
        }

        using DbConnection conn = type switch
        {
            "mysql" => new MySqlConnection(connectionString),
            "postgres" => new NpgsqlConnection(connectionString),
            "mssql" => new SqlConnection(connectionString),
            _ => throw new NotSupportedException($"SQL Typ '{type}' wird nicht unterstützt.")
        };

        await conn.OpenAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = sql;

        if (action == "query")
        {
            using var reader = await cmd.ExecuteReaderAsync(ct);
            var results = new List<Dictionary<string, object>>();
            while (await reader.ReadAsync(ct))
            {
                var row = new Dictionary<string, object>();
                for (int i = 0; i < reader.FieldCount; i++)
                {
                    row[reader.GetName(i)] = reader.GetValue(i);
                }
                results.Add(row);
            }
            return ToolResult.Ok(JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
        }
        else
        {
            var affected = await cmd.ExecuteNonQueryAsync(ct);
            return ToolResult.Ok($"{affected} Zeilen betroffen.");
        }
    }

    private async Task<ToolResult> HandleMongoAsync(string action, JsonElement input, string connectionString, AgentToolContext context, CancellationToken ct)
    {
        var collectionName = input.TryGetProperty("collection", out var c) ? c.GetString() : null;
        if (string.IsNullOrWhiteSpace(collectionName)) return ToolResult.Fail("'collection' ist erforderlich.");

        if (!IsCollectionAllowed(collectionName, context))
        {
            return ToolResult.Fail($"Sicherheitsfehler: Zugriff auf Collection '{collectionName}' ist nicht erlaubt.");
        }

        var accessLevel = GetAccessLevel(context);
        var client = new MongoClient(connectionString);
        var dbName = new MongoUrl(connectionString).DatabaseName;
        var db = client.GetDatabase(dbName);
        var collection = db.GetCollection<BsonDocument>(collectionName);

        if (action == "query")
        {
            var filterJson = input.TryGetProperty("filter", out var f) ? f.GetString() : "{}";
            var filter = BsonDocument.Parse(filterJson);
            var docs = await collection.Find(filter).Limit(100).ToListAsync(ct);
            var results = docs.Select(d => d.ToJson()).ToList();
            return ToolResult.Ok("[" + string.Join(",", results) + "]");
        }
        else if (action == "insert")
        {
            if (accessLevel == DatabaseAccessLevel.ReadOnly) return ToolResult.Fail("Sicherheitsfehler: Schreibzugriff deaktiviert.");
            var docJson = input.TryGetProperty("document", out var d) ? d.GetString() : throw new ArgumentException("'document' erforderlich.");
            var doc = BsonDocument.Parse(docJson);
            await collection.InsertOneAsync(doc, cancellationToken: ct);
            return ToolResult.Ok("Dokument erfolgreich eingefügt.");
        }
        else // upsert
        {
             if (accessLevel == DatabaseAccessLevel.ReadOnly) return ToolResult.Fail("Sicherheitsfehler: Schreibzugriff deaktiviert.");
             return ToolResult.Fail("Upsert für MongoDB noch nicht voll implementiert.");
        }
    }

    private DatabaseAccessLevel GetAccessLevel(AgentToolContext context)
    {
        if (context.ToolConfig.TryGetValue("accessLevel", out var val) && val != null)
        {
            if (Enum.TryParse<DatabaseAccessLevel>(val.ToString(), true, out var level))
                return level;
        }

        // Rückfall auf altes 'allowWrite' für Abwärtskompatibilität
        if (context.ToolConfig.TryGetValue("allowWrite", out var aw) && aw is JsonElement je && je.GetBoolean())
        {
            return DatabaseAccessLevel.ReadWrite;
        }

        return DatabaseAccessLevel.ReadOnly;
    }

    internal static List<string> GetAllowedTables(AgentToolContext context)
    {
        if (!context.ToolConfig.TryGetValue("allowedTables", out var val) || val is not JsonElement je
            || je.ValueKind != JsonValueKind.Array)
        {
            return [];
        }

        return je.EnumerateArray()
            .Select(x => x.GetString())
            .Where(s => !string.IsNullOrWhiteSpace(s))
            .ToList()!;
    }

    /// <summary>
    /// MongoDB kennt kein SQL — hier genügt der exakte Vergleich des Collection-Namens.
    /// Bisher wurde auch hier per Teilzeichenkette geprüft.
    /// </summary>
    private static bool IsCollectionAllowed(string collection, AgentToolContext context)
        => GetAllowedTables(context).Contains(collection, StringComparer.OrdinalIgnoreCase);
}
