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

namespace ClawdDotNet.Core.Tasks;

/// <summary>
/// Der Ausführungszustand des Taskboards in der Instanz-Datenbank.
///
/// Der Kern ist <see cref="TryClaimAsync"/>: ein bedingtes <c>UPDATE</c>, das genau eine
/// Zeile trifft und damit garantiert, dass nie zwei Läufe denselben Termin ziehen. Genau
/// das kann ein Dateisystem nicht verlässlich — deshalb liegt der Zustand hier und nicht
/// bei den Task-Dateien.
///
/// Zeitangaben werden als sortierbares ISO-UTC (<c>"O"</c>) abgelegt, damit
/// Zeichenketten-Vergleiche in SQL derselben Ordnung folgen wie die Zeit selbst.
/// </summary>
public sealed class SqliteTaskRepository : ITaskRepository
{
    private readonly SqliteStorage _storage;

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

    public Task<TaskItem> UpsertAsync(TaskItem task, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            var now = DateTime.UtcNow;
            var existing = await ReadAsync(conn, task.Id, ct);

            if (existing is null)
            {
                using var insert = conn.CreateCommand();
                insert.CommandText = """
                    INSERT INTO Tasks
                        (Id, Title, Status, Type, Priority, Assignee, WhenKind, WhenValue, WhenTz,
                         RequireApproval, Acceptance, BlockedBy, OnlyWhenMarketOpen, Body, FileName,
                         ToolName, JobTypeId, LastOccurrence, ClaimToken, ClaimedAt, CreatedAt, UpdatedAt)
                    VALUES
                        (@id, @title, @status, @type, @priority, @assignee, @whenKind, @whenValue, @whenTz,
                         @requireApproval, @acceptance, @blockedBy, @market, @body, @fileName,
                         @toolName, @jobType, NULL, NULL, NULL, @createdAt, @updatedAt);
                    """;
                BindDefinition(insert, task);
                insert.Parameters.AddWithValue("@status", TaskText.Of(task.Status));
                insert.Parameters.AddWithValue("@createdAt", Format(now));
                insert.Parameters.AddWithValue("@updatedAt", Format(now));
                await insert.ExecuteNonQueryAsync(ct);
            }
            else
            {
                // Nur die Definition aktualisieren. Status und Ausführungszustand
                // (Marker, Claim) gehören der DB — ein Re-Import darf einen laufenden
                // oder abgeschlossenen Zustand nicht zurücksetzen. Status-Änderungen
                // laufen über SetStatusAsync bzw. den Scanner.
                using var update = conn.CreateCommand();
                update.CommandText = """
                    UPDATE Tasks
                       SET Title = @title, Type = @type, Priority = @priority, Assignee = @assignee,
                           WhenKind = @whenKind, WhenValue = @whenValue, WhenTz = @whenTz,
                           RequireApproval = @requireApproval, Acceptance = @acceptance,
                           BlockedBy = @blockedBy, OnlyWhenMarketOpen = @market, Body = @body,
                           FileName = @fileName, ToolName = @toolName, JobTypeId = @jobType,
                           UpdatedAt = @updatedAt
                     WHERE Id = @id
                    """;
                BindDefinition(update, task);
                update.Parameters.AddWithValue("@updatedAt", Format(now));
                await update.ExecuteNonQueryAsync(ct);
            }

            return (await ReadAsync(conn, task.Id, ct))!;
        }, ct);

    public async Task<TaskItem?> GetAsync(string id, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        return await ReadAsync(conn, id, ct);
    }

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

        var sql = new StringBuilder("SELECT " + Columns + " FROM Tasks WHERE 1 = 1");

        if (query.Status is { } status)
        {
            sql.Append(" AND Status = @status");
            cmd.Parameters.AddWithValue("@status", TaskText.Of(status));
        }
        else if (!query.IncludeArchived)
        {
            sql.Append(" AND Status <> 'archived'");
        }

        if (!string.IsNullOrWhiteSpace(query.Assignee))
        {
            sql.Append(" AND Assignee = @assignee COLLATE NOCASE");
            cmd.Parameters.AddWithValue("@assignee", query.Assignee.Trim());
        }

        if (!string.IsNullOrWhiteSpace(query.Search))
        {
            sql.Append(" AND (Title LIKE @search ESCAPE '\\' COLLATE NOCASE"
                     + " OR Body LIKE @search ESCAPE '\\' COLLATE NOCASE)");
            cmd.Parameters.AddWithValue("@search", "%" + Escape(query.Search.Trim()) + "%");
        }

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

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

        return results;
    }

    public Task<bool> TryClaimAsync(
        string id, string occurrenceKey, string claimToken,
        DateTime now, DateTime leaseCutoff, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                UPDATE Tasks
                   SET ClaimToken = @token, ClaimedAt = @now, Status = 'in_progress',
                       LastOccurrence = @occ, UpdatedAt = @now
                 WHERE Id = @id
                   AND Status = 'todo'
                   AND (LastOccurrence IS NULL OR LastOccurrence < @occ)
                   AND (ClaimToken IS NULL OR ClaimedAt < @leaseCutoff)
                """;
            cmd.Parameters.AddWithValue("@token", claimToken);
            cmd.Parameters.AddWithValue("@now", Format(now));
            cmd.Parameters.AddWithValue("@occ", occurrenceKey);
            cmd.Parameters.AddWithValue("@id", id);
            cmd.Parameters.AddWithValue("@leaseCutoff", Format(leaseCutoff));

            return await cmd.ExecuteNonQueryAsync(ct) == 1;
        }, ct);

    public Task<bool> CompleteClaimAsync(
        string id, string claimToken, TaskItemStatus finalStatus, DateTime now, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                UPDATE Tasks
                   SET Status = @status, ClaimToken = NULL, ClaimedAt = NULL, UpdatedAt = @now
                 WHERE Id = @id AND ClaimToken = @token
                """;
            cmd.Parameters.AddWithValue("@status", TaskText.Of(finalStatus));
            cmd.Parameters.AddWithValue("@now", Format(now));
            cmd.Parameters.AddWithValue("@id", id);
            cmd.Parameters.AddWithValue("@token", claimToken);

            return await cmd.ExecuteNonQueryAsync(ct) > 0;
        }, ct);

    public Task<bool> SetStatusAsync(string id, TaskItemStatus status, DateTime now, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = "UPDATE Tasks SET Status = @status, UpdatedAt = @now WHERE Id = @id";
            cmd.Parameters.AddWithValue("@status", TaskText.Of(status));
            cmd.Parameters.AddWithValue("@now", Format(now));
            cmd.Parameters.AddWithValue("@id", id);

            return await cmd.ExecuteNonQueryAsync(ct) > 0;
        }, ct);

    public Task<int> ReleaseStaleClaimsAsync(DateTime leaseCutoff, DateTime now, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                UPDATE Tasks
                   SET Status = 'todo', ClaimToken = NULL, ClaimedAt = NULL, UpdatedAt = @now
                 WHERE Status = 'in_progress' AND ClaimToken IS NOT NULL AND ClaimedAt < @leaseCutoff
                """;
            cmd.Parameters.AddWithValue("@now", Format(now));
            cmd.Parameters.AddWithValue("@leaseCutoff", Format(leaseCutoff));

            return await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

    public async Task<IReadOnlyList<TaskItem>> ListBlockedByAsync(string blockerId, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + Columns
            + " FROM Tasks WHERE BlockedBy LIKE @needle ESCAPE '\\'";
        cmd.Parameters.AddWithValue("@needle", "%|" + Escape(blockerId.Trim()) + "|%");

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

        return results;
    }

    public async Task<int> CountAsync(CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM Tasks";
        return Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
    }

    // ─── Hilfsfunktionen ───

    private const string Columns =
        "Id, Title, Status, Type, Priority, Assignee, WhenKind, WhenValue, WhenTz, " +
        "RequireApproval, Acceptance, BlockedBy, OnlyWhenMarketOpen, Body, FileName, " +
        "ToolName, JobTypeId, LastOccurrence, ClaimToken, ClaimedAt, CreatedAt, UpdatedAt";

    private static void BindDefinition(SqliteCommand cmd, TaskItem task)
    {
        cmd.Parameters.AddWithValue("@id", task.Id);
        cmd.Parameters.AddWithValue("@title", task.Title);
        cmd.Parameters.AddWithValue("@type", TaskText.Of(task.Type));
        cmd.Parameters.AddWithValue("@priority", Math.Clamp(task.Priority, 1, 5));
        cmd.Parameters.AddWithValue("@assignee", task.Assignee);
        cmd.Parameters.AddWithValue("@whenKind", (object?)(task.When is null ? null : TaskText.Of(task.When.Kind)) ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@whenValue", (object?)task.When?.Value ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@whenTz", (object?)task.When?.TimeZone ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@requireApproval", task.RequireApproval ? 1 : 0);
        cmd.Parameters.AddWithValue("@acceptance", task.Acceptance);
        cmd.Parameters.AddWithValue("@blockedBy", SerializeBlockedBy(task.BlockedBy));
        cmd.Parameters.AddWithValue("@market", task.OnlyWhenMarketOpen ? 1 : 0);
        cmd.Parameters.AddWithValue("@body", task.Body);
        cmd.Parameters.AddWithValue("@fileName", task.FileName);
        cmd.Parameters.AddWithValue("@toolName", task.ToolName);
        cmd.Parameters.AddWithValue("@jobType", task.JobTypeId);
    }

    private static async Task<TaskItem?> ReadAsync(SqliteConnection conn, string id, CancellationToken ct)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + Columns + " FROM Tasks WHERE Id = @id LIMIT 1";
        cmd.Parameters.AddWithValue("@id", id);

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

    private static TaskItem Read(SqliteDataReader r)
    {
        TaskWhen? when = null;
        if (!r.IsDBNull(6) && TaskText.WhenKind(r.GetString(6)) is { } kind)
        {
            when = new TaskWhen
            {
                Kind = kind,
                Value = r.IsDBNull(7) ? "" : r.GetString(7),
                TimeZone = r.IsDBNull(8) ? "" : r.GetString(8)
            };
        }

        return new TaskItem
        {
            Id = r.GetString(0),
            Title = r.GetString(1),
            Status = TaskText.Status(r.GetString(2)),
            Type = TaskText.Type(r.GetString(3)),
            Priority = r.GetInt32(4),
            Assignee = r.GetString(5),
            When = when,
            RequireApproval = r.GetInt32(9) != 0,
            Acceptance = r.GetString(10),
            BlockedBy = DeserializeBlockedBy(r.GetString(11)),
            OnlyWhenMarketOpen = r.GetInt32(12) != 0,
            Body = r.GetString(13),
            FileName = r.GetString(14),
            ToolName = r.GetString(15),
            JobTypeId = r.GetString(16),
            LastOccurrence = r.IsDBNull(17) ? null : r.GetString(17),
            ClaimToken = r.IsDBNull(18) ? null : r.GetString(18),
            ClaimedAt = r.IsDBNull(19) ? null : Parse(r.GetString(19)),
            CreatedAt = Parse(r.GetString(20)),
            UpdatedAt = Parse(r.GetString(21))
        };
    }

    /// <summary>Blocker als "|a|b|" — die Begrenzer erlauben eine Suche nach ganzen Ids,
    /// ohne dass <c>t-1</c> auch <c>t-10</c> trifft.</summary>
    private static string SerializeBlockedBy(IReadOnlyList<string> ids)
    {
        var cleaned = ids
            .Select(x => x.Trim().Replace("|", ""))
            .Where(x => x.Length > 0)
            .Distinct()
            .ToList();

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

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

    private static string Escape(string value) => value
        .Replace(@"\", @"\\")
        .Replace("%", @"\%")
        .Replace("_", @"\_");

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

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