using ClawdDotNet.Core.Storage;

namespace ClawdDotNet.Core.Tasks;

/// <summary>
/// Die Brücke der Wahrheitsaufteilung: Markdown-Dateien unter <c>SharedWorkspace/tasks/</c>
/// halten die Definition, die DB (<see cref="ITaskRepository"/>) den Ausführungszustand.
/// Dieser Dienst hält beide im Gleichschritt und wird von drei Seiten genutzt — dem
/// Agenten-Tool, dem Scanner und dem Start (Reconciliation).
///
/// Bewusst zustandslos über den Aufrufen: nur Repository und Verzeichnis, keine
/// gepufferten Aufgaben. So kann jede Seite ihn nach Bedarf erzeugen.
/// </summary>
public sealed class TaskboardService
{
    private readonly ITaskRepository _repo;
    private readonly string _tasksDir;

    public TaskboardService(ITaskRepository repo, string tasksDirectory)
    {
        _repo = repo;
        _tasksDir = tasksDirectory;
    }

    public string TasksDirectory => _tasksDir;

    // ─── Lesen (direkt aus der DB) ───

    public Task<TaskItem?> GetAsync(string id, CancellationToken ct) => _repo.GetAsync(id, ct);

    public Task<IReadOnlyList<TaskItem>> ListAsync(TaskQuery query, CancellationToken ct)
        => _repo.ListAsync(query, ct);

    // ─── Anlegen ───

    /// <summary>
    /// Legt eine Aufgabe an: vergibt (falls nötig) eine stabile Id, schreibt die Datei und
    /// spiegelt sie in die DB. Datei und DB entstehen in einem Zug.
    /// </summary>
    public async Task<TaskItem> CreateAsync(TaskItem definition, CancellationToken ct)
    {
        var id = string.IsNullOrWhiteSpace(definition.Id) ? await NewIdAsync(ct) : definition.Id.Trim();
        var fileName = string.IsNullOrWhiteSpace(definition.FileName)
            ? BuildFileName(definition.Title, id)
            : definition.FileName;

        var toWrite = definition with { Id = id, FileName = fileName };
        WriteFile(toWrite);
        return await _repo.UpsertAsync(toWrite, ct);
    }

    // ─── Ändern ───

    /// <summary>
    /// Ändert die Definition einer Aufgabe (Tool-Aktion). Schreibt Datei und DB. Eine
    /// Status-Änderung wird zusätzlich explizit gesetzt — der Import spiegelt nur die
    /// Definition und lässt den Ausführungszustand absichtlich unangetastet, damit ein
    /// blinder Re-Import einen laufenden Zustand nicht zurücksetzt.
    /// </summary>
    public async Task<TaskItem?> UpdateAsync(
        string id, Func<TaskItem, TaskItem> mutate, CancellationToken ct)
    {
        var current = await _repo.GetAsync(id, ct);
        if (current is null) return null;

        var updated = mutate(current) with { Id = id, FileName = current.FileName };
        WriteFile(updated);
        await _repo.UpsertAsync(updated, ct);

        if (updated.Status != current.Status)
            await _repo.SetStatusAsync(id, updated.Status, DateTime.UtcNow, ct);

        return await _repo.GetAsync(id, ct);
    }

    /// <summary>Hängt einen Kommentar (Ergebnis, Kritik) an den Rumpf an.</summary>
    public Task<TaskItem?> AddCommentAsync(string id, string author, string text, CancellationToken ct)
        => UpdateAsync(id, current =>
        {
            var stamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
            var block = $"---\n**{author}** ({stamp}):\n\n{text.Trim()}";
            var body = string.IsNullOrWhiteSpace(current.Body) ? block : $"{current.Body.TrimEnd()}\n\n{block}";
            return current with { Body = body };
        }, ct);

    // ─── Import (Datei → DB) ───

    /// <summary>
    /// Liest alle Task-Dateien und spiegelt sie in die DB. DB-Zeilen, deren Datei
    /// verschwunden ist, werden archiviert (nicht gelöscht — die Historie bleibt). Gibt
    /// die Zahl der importierten Dateien zurück.
    /// </summary>
    public async Task<int> ImportAllAsync(CancellationToken ct)
    {
        if (!Directory.Exists(_tasksDir))
            return 0;

        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var imported = 0;

        foreach (var path in Directory.EnumerateFiles(_tasksDir, "*.md"))
        {
            var task = await ImportFileAsync(path, ct);
            if (task is not null) { seen.Add(task.Id); imported++; }
        }

        // Verwaiste Zeilen archivieren. Der Deckel von 500 ist für den Start unkritisch;
        // wächst das Board darüber hinaus, gehört ohnehin das Aufräumen (siehe Konzept) her.
        var known = await _repo.ListAsync(new TaskQuery { IncludeArchived = true, Limit = 500 }, ct);
        foreach (var task in known)
            if (task.Status != TaskItemStatus.Archived && !seen.Contains(task.Id))
                await _repo.SetStatusAsync(task.Id, TaskItemStatus.Archived, DateTime.UtcNow, ct);

        return imported;
    }

    /// <summary>Importiert eine einzelne Datei. Eine von Hand angelegte Datei ohne
    /// <c>id</c> bekommt eine zugewiesen und wird einmalig kanonisch zurückgeschrieben.</summary>
    public async Task<TaskItem?> ImportFileAsync(string path, CancellationToken ct)
    {
        string text;
        try { text = AtomicFile.ReadAllText(path); }
        catch { return null; }

        if (!TaskFrontmatter.TryParse(text, out var definition, out _))
            return null;

        var fileName = Path.GetFileName(path);

        if (string.IsNullOrWhiteSpace(definition.Id))
        {
            definition = definition with { Id = await NewIdAsync(ct), FileName = fileName };
            WriteFile(definition); // Id festschreiben, damit sie den nächsten Start überlebt
        }
        else
        {
            definition = definition with { FileName = fileName };
        }

        return await _repo.UpsertAsync(definition, ct);
    }

    // ─── Hilfsfunktionen ───

    private void WriteFile(TaskItem definition)
    {
        Directory.CreateDirectory(_tasksDir);
        var path = Path.Combine(_tasksDir, definition.FileName);
        AtomicFile.WriteAllText(path, TaskFrontmatter.Serialize(definition));
    }

    private async Task<string> NewIdAsync(CancellationToken ct)
    {
        for (var i = 0; i < 5; i++)
        {
            var id = "t-" + Guid.NewGuid().ToString("N")[..6];
            if (await _repo.GetAsync(id, ct) is null)
                return id;
        }
        return "t-" + Guid.NewGuid().ToString("N")[..12];
    }

    private static string BuildFileName(string title, string id)
    {
        var slug = Slug(title);
        var suffix = id.StartsWith("t-", StringComparison.Ordinal) ? id[2..] : id;
        return (slug.Length == 0 ? "task" : slug) + "-" + suffix + ".md";
    }

    private static string Slug(string title)
    {
        var chars = title.Trim().ToLowerInvariant()
            .Select(c => char.IsLetterOrDigit(c) ? c : '-')
            .ToArray();
        var slug = new string(chars);

        while (slug.Contains("--"))
            slug = slug.Replace("--", "-");
        slug = slug.Trim('-');

        if (slug.Length > 40)
            slug = slug[..40].Trim('-');

        return slug;
    }
}
