using System.Text;
using ClawdDotNet.Core.Storage;
using Microsoft.Data.Sqlite;

namespace ClawdDotNet.Core.Memory;

/// <summary>
/// Ablage der Erinnerungen in der Instanz-Datenbank.
///
/// Bewusst typisierte Spalten statt JSON in einem Wert-Feld: Nur so lassen sich
/// Erinnerungen gezielt filtern, sortieren und später auswerten. Das Schema ist
/// absichtlich einfach gehalten, damit eine MySQL-Variante später dieselbe Struktur
/// mit nur wenigen Dialektunterschieden bekommen kann.
/// </summary>
public sealed class SqliteMemoryRepository : IMemoryRepository
{
    private readonly SqliteStorage _storage;

    public SqliteMemoryRepository(SqliteStorage storage) => _storage = storage;

    public Task<MemoryEntry> RememberAsync(MemoryEntry entry, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            var now = DateTime.UtcNow;
            var scope = entry.Scope.ToString();
            var owner = OwnerFor(entry.Scope, entry.OwnerId);
            var tags = SerializeTags(entry.Tags);

            // Mit Schlüssel: bestehende Erinnerung aktualisieren statt eine zweite anlegen.
            if (!string.IsNullOrWhiteSpace(entry.Key))
            {
                var existing = await ReadByKeyAsync(conn, entry.Scope, owner, entry.Key!, ct);
                if (existing is not null)
                {
                    using var update = conn.CreateCommand();
                    update.CommandText = """
                        UPDATE Memories
                           SET Category = @category, Subject = @subject, Content = @content,
                               Tags = @tags, Importance = @importance, UpdatedAt = @updatedAt
                         WHERE Id = @id
                        """;
                    update.Parameters.AddWithValue("@category", MemoryCategory.Normalize(entry.Category));
                    update.Parameters.AddWithValue("@subject", entry.Subject);
                    update.Parameters.AddWithValue("@content", entry.Content);
                    update.Parameters.AddWithValue("@tags", tags);
                    update.Parameters.AddWithValue("@importance", ClampImportance(entry.Importance));
                    update.Parameters.AddWithValue("@updatedAt", Format(now));
                    update.Parameters.AddWithValue("@id", existing.Id);
                    await update.ExecuteNonQueryAsync(ct);

                    return existing with
                    {
                        Category = MemoryCategory.Normalize(entry.Category),
                        Subject = entry.Subject,
                        Content = entry.Content,
                        Tags = entry.Tags,
                        Importance = ClampImportance(entry.Importance),
                        UpdatedAt = now
                    };
                }
            }

            using var insert = conn.CreateCommand();
            insert.CommandText = """
                INSERT INTO Memories
                    (Scope, OwnerId, Category, Subject, MemoryKey, Content, Tags, Importance,
                     CreatedAt, UpdatedAt, CreatedBy)
                VALUES
                    (@scope, @owner, @category, @subject, @key, @content, @tags, @importance,
                     @createdAt, @updatedAt, @createdBy);
                SELECT last_insert_rowid();
                """;
            insert.Parameters.AddWithValue("@scope", scope);
            insert.Parameters.AddWithValue("@owner", owner);
            insert.Parameters.AddWithValue("@category", MemoryCategory.Normalize(entry.Category));
            insert.Parameters.AddWithValue("@subject", entry.Subject);
            insert.Parameters.AddWithValue("@key", (object?)NullIfBlank(entry.Key) ?? DBNull.Value);
            insert.Parameters.AddWithValue("@content", entry.Content);
            insert.Parameters.AddWithValue("@tags", tags);
            insert.Parameters.AddWithValue("@importance", ClampImportance(entry.Importance));
            insert.Parameters.AddWithValue("@createdAt", Format(now));
            insert.Parameters.AddWithValue("@updatedAt", Format(now));
            insert.Parameters.AddWithValue("@createdBy", entry.CreatedBy);

            var id = Convert.ToInt64(await insert.ExecuteScalarAsync(ct));

            return entry with
            {
                Id = id,
                OwnerId = owner,
                Category = MemoryCategory.Normalize(entry.Category),
                Importance = ClampImportance(entry.Importance),
                CreatedAt = now,
                UpdatedAt = now
            };
        }, ct);

    public async Task<IReadOnlyList<MemoryEntry>> RecallAsync(MemoryQuery query, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();

        var sql = new StringBuilder("""
            SELECT Id, Scope, OwnerId, Category, Subject, MemoryKey, Content, Tags,
                   Importance, CreatedAt, UpdatedAt, CreatedBy
              FROM Memories
             WHERE Scope = @scope AND OwnerId = @owner AND Importance >= @minImportance
            """);

        cmd.Parameters.AddWithValue("@scope", query.Scope.ToString());
        cmd.Parameters.AddWithValue("@owner", OwnerFor(query.Scope, query.OwnerId));
        cmd.Parameters.AddWithValue("@minImportance", ClampImportance(query.MinImportance));

        if (NullIfBlank(query.Subject) is { } subject)
        {
            sql.Append(" AND Subject = @subject COLLATE NOCASE");
            cmd.Parameters.AddWithValue("@subject", subject);
        }

        if (NullIfBlank(query.Category) is { } category)
        {
            sql.Append(" AND Category = @category");
            cmd.Parameters.AddWithValue("@category", MemoryCategory.Normalize(category));
        }

        if (NullIfBlank(query.Search) is { } search)
        {
            // Freitext über Betreff, Inhalt und Schlagworte.
            sql.Append("""
                 AND (Subject LIKE @search ESCAPE '\' COLLATE NOCASE
                   OR Content LIKE @search ESCAPE '\' COLLATE NOCASE
                   OR Tags    LIKE @search ESCAPE '\' COLLATE NOCASE)
                """);
            cmd.Parameters.AddWithValue("@search", "%" + Escape(search) + "%");
        }

        for (var i = 0; i < query.Tags.Count; i++)
        {
            var tag = NullIfBlank(query.Tags[i]);
            if (tag is null) continue;

            // Schlagworte liegen als "|a|b|c|" — die Begrenzer verhindern Teiltreffer.
            sql.Append($" AND Tags LIKE @tag{i} ESCAPE '\\' COLLATE NOCASE");
            cmd.Parameters.AddWithValue($"@tag{i}", $"%|{Escape(tag.ToLowerInvariant())}|%");
        }

        // Wichtiges zuerst, dann das Aktuellste — damit eine Kappung das Richtige behält.
        sql.Append(" ORDER BY Importance DESC, UpdatedAt DESC LIMIT @limit");
        cmd.Parameters.AddWithValue("@limit", Math.Clamp(query.Limit, 1, 200));

        cmd.CommandText = sql.ToString();

        var results = new List<MemoryEntry>();
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
            results.Add(Read(reader));

        return results;
    }

    public async Task<MemoryEntry?> GetByKeyAsync(
        MemoryScope scope, string ownerId, string key, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        return await ReadByKeyAsync(conn, scope, OwnerFor(scope, ownerId), key, ct);
    }

    public Task<bool> ForgetAsync(long id, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = "DELETE FROM Memories WHERE Id = @id";
            cmd.Parameters.AddWithValue("@id", id);
            return await cmd.ExecuteNonQueryAsync(ct) > 0;
        }, ct);

    public Task<int> ForgetBySubjectAsync(
        MemoryScope scope, string ownerId, string subject, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                DELETE FROM Memories
                 WHERE Scope = @scope AND OwnerId = @owner AND Subject = @subject COLLATE NOCASE
                """;
            cmd.Parameters.AddWithValue("@scope", scope.ToString());
            cmd.Parameters.AddWithValue("@owner", OwnerFor(scope, ownerId));
            cmd.Parameters.AddWithValue("@subject", subject);
            return await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

    public async Task<IReadOnlyList<(string Subject, int Count)>> ListSubjectsAsync(
        MemoryScope scope, string ownerId, int limit, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            SELECT Subject, COUNT(*) AS Anzahl
              FROM Memories
             WHERE Scope = @scope AND OwnerId = @owner
             GROUP BY Subject COLLATE NOCASE
             ORDER BY Anzahl DESC, Subject
             LIMIT @limit
            """;
        cmd.Parameters.AddWithValue("@scope", scope.ToString());
        cmd.Parameters.AddWithValue("@owner", OwnerFor(scope, ownerId));
        cmd.Parameters.AddWithValue("@limit", Math.Clamp(limit, 1, 500));

        var results = new List<(string, int)>();
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
            results.Add((reader.GetString(0), reader.GetInt32(1)));

        return results;
    }

    public async Task<int> CountAsync(MemoryScope scope, string ownerId, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM Memories WHERE Scope = @scope AND OwnerId = @owner";
        cmd.Parameters.AddWithValue("@scope", scope.ToString());
        cmd.Parameters.AddWithValue("@owner", OwnerFor(scope, ownerId));

        return Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
    }

    // ─── Hilfsfunktionen ───

    private static async Task<MemoryEntry?> ReadByKeyAsync(
        SqliteConnection conn, MemoryScope scope, string owner, string key, CancellationToken ct)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            SELECT Id, Scope, OwnerId, Category, Subject, MemoryKey, Content, Tags,
                   Importance, CreatedAt, UpdatedAt, CreatedBy
              FROM Memories
             WHERE Scope = @scope AND OwnerId = @owner AND MemoryKey = @key
             LIMIT 1
            """;
        cmd.Parameters.AddWithValue("@scope", scope.ToString());
        cmd.Parameters.AddWithValue("@owner", owner);
        cmd.Parameters.AddWithValue("@key", key);

        await using var reader = await cmd.ExecuteReaderAsync(ct);
        return await reader.ReadAsync(ct) ? Read(reader) : null;
    }

    private static MemoryEntry Read(SqliteDataReader reader) => new()
    {
        Id = reader.GetInt64(0),
        Scope = Enum.TryParse<MemoryScope>(reader.GetString(1), out var s) ? s : MemoryScope.Agent,
        OwnerId = reader.GetString(2),
        Category = reader.GetString(3),
        Subject = reader.GetString(4),
        Key = reader.IsDBNull(5) ? null : reader.GetString(5),
        Content = reader.GetString(6),
        Tags = DeserializeTags(reader.GetString(7)),
        Importance = reader.GetInt32(8),
        CreatedAt = Parse(reader.GetString(9)),
        UpdatedAt = Parse(reader.GetString(10)),
        CreatedBy = reader.GetString(11)
    };

    /// <summary>
    /// Schlagworte als "|a|b|c|". Die Begrenzer erlauben eine Suche nach ganzen
    /// Schlagworten, ohne dass "news" auch "newsletter" trifft.
    /// </summary>
    private static string SerializeTags(IReadOnlyList<string> tags)
    {
        var cleaned = tags
            .Select(t => t.Trim().ToLowerInvariant().Replace("|", ""))
            .Where(t => t.Length > 0)
            .Distinct()
            .ToList();

        return cleaned.Count == 0 ? "" : "|" + string.Join("|", cleaned) + "|";
    }

    private static IReadOnlyList<string> DeserializeTags(string raw)
        => raw.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

    private static string OwnerFor(MemoryScope scope, string ownerId)
        => scope == MemoryScope.Shared ? "" : ownerId;

    private static int ClampImportance(int value) => Math.Clamp(value, 1, 5);

    private static string? NullIfBlank(string? value)
        => string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    /// <summary>
    /// Maskiert LIKE-Platzhalter, damit ein Suchtext wörtlich gesucht wird.
    /// Werden die Zeichen stattdessen entfernt, würde eine Suche nach "%" zu einem
    /// leeren Muster und damit zu einem Treffer auf alles.
    /// </summary>
    private static string Escape(string value) => value
        .Replace(@"\", @"\\")
        .Replace("%", @"\%")
        .Replace("_", @"\_");

    private static string Format(DateTime value) => value.ToString("O");

    private static DateTime Parse(string value)
        => DateTime.TryParse(value, null, System.Globalization.DateTimeStyles.RoundtripKind, out var dt)
            ? dt
            : DateTime.MinValue;
}
