using ClawdDotNet.Core.Audit;
using ClawdDotNet.Core.Tasks;
using Microsoft.Extensions.Logging;

namespace ClawdDotNet.Core.Staging;

public enum StagingResultKind { NotFound, AlreadyDecided, Executed, Failed, Rejected }

public readonly record struct StagingResult(StagingResultKind Kind, string Message)
{
    public static StagingResult NotFound { get; } = new(StagingResultKind.NotFound, "Vorschlag nicht gefunden.");
    public static StagingResult AlreadyDecided { get; } =
        new(StagingResultKind.AlreadyDecided, "Der Vorschlag wurde bereits entschieden.");
}

/// <summary>
/// Die Freigabe-/Ablehnungs-Seite von A2 — die API, die die Review-Oberfläche aufruft.
///
/// Kein pausierter Lauf: Bei Freigabe wird der <b>eingefrorene</b> Aufruf direkt
/// ausgeführt (Plan-Freeze), das Ergebnis festgehalten und der Agent über einen
/// Folge-Task (A1) mit dem Ergebnis geweckt. Jede Entscheidung wird als Approval-Record
/// ins Audit-Log (A3) geschrieben.
/// </summary>
public sealed class StagingService
{
    private readonly IStagingRepository _repo;
    private readonly IFrozenCallExecutor _executor;
    private readonly TaskboardService _board;
    private readonly IAuditRepository? _audit;
    private readonly ILogger _logger;

    public StagingService(
        IStagingRepository repo,
        IFrozenCallExecutor executor,
        TaskboardService board,
        ILoggerFactory loggerFactory,
        IAuditRepository? audit = null)
    {
        _repo = repo;
        _executor = executor;
        _board = board;
        _audit = audit;
        _logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Staging");
    }

    public Task<IReadOnlyList<StagedCall>> ListPendingAsync(CancellationToken ct) => _repo.ListPendingAsync(ct);
    public Task<StagedCall?> GetAsync(long id, CancellationToken ct) => _repo.GetAsync(id, ct);
    public Task<int> CountPendingAsync(CancellationToken ct) => _repo.CountPendingAsync(ct);

    public async Task<StagingResult> ApproveAsync(long id, string decidedBy, CancellationToken ct)
    {
        var call = await _repo.GetAsync(id, ct);
        if (call is null) return StagingResult.NotFound;

        // Atomar beanspruchen — ein zweiter Reviewer läuft ins Leere, bevor irgendetwas
        // ausgeführt wird.
        if (!await _repo.TryTransitionAsync(id, StagingStatus.Pending, StagingStatus.Approved, decidedBy, null, DateTime.UtcNow, ct))
            return StagingResult.AlreadyDecided;

        string result;
        StagingStatus final;
        try
        {
            // Genau der eingefrorene Aufruf — nie eine neu formulierte Fassung.
            result = await _executor.ExecuteApprovedCallAsync(
                call.AgentId, call.Tool, call.ArgumentsJson, Guid.NewGuid().ToString("N"), ct);
            final = StagingStatus.Executed;
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            result = $"Ausführung fehlgeschlagen: {ex.Message}";
            final = StagingStatus.Failed;
            _logger.LogError(ex, "Freigegebener Aufruf #{Id} ({Tool}) schlug fehl", id, call.Tool);
        }

        await _repo.FinalizeAsync(id, final, Cap(result, 500), DateTime.UtcNow, ct);
        await RecordAuditAsync(call, decidedBy, final == StagingStatus.Executed ? AuditStatus.Ok : AuditStatus.Error,
            $"Freigegeben von {decidedBy}", ct);
        await WakeAgentAsync(call,
            $"Freigabe-Ergebnis: {call.Tool}",
            $"[Freigabe] Deine vorgelegte Aktion '{Label(call)}' wurde freigegeben und ausgeführt.\n\n" +
            $"Ergebnis:\n{result}\n\nSetze deine Arbeit fort.", ct);

        return new StagingResult(
            final == StagingStatus.Executed ? StagingResultKind.Executed : StagingResultKind.Failed, result);
    }

    public async Task<StagingResult> RejectAsync(long id, string decidedBy, string reason, CancellationToken ct)
    {
        var call = await _repo.GetAsync(id, ct);
        if (call is null) return StagingResult.NotFound;

        if (!await _repo.TryTransitionAsync(id, StagingStatus.Pending, StagingStatus.Rejected, decidedBy, reason, DateTime.UtcNow, ct))
            return StagingResult.AlreadyDecided;

        await RecordAuditAsync(call, decidedBy, AuditStatus.Denied, $"Abgelehnt von {decidedBy}: {reason}", ct);
        await WakeAgentAsync(call,
            $"Freigabe abgelehnt: {call.Tool}",
            $"[Ablehnung] Deine vorgelegte Aktion '{Label(call)}' wurde abgelehnt.\n\n" +
            $"Grund: {reason}\n\nFühre sie nicht erneut ohne Rücksprache aus.", ct);

        return new StagingResult(StagingResultKind.Rejected, reason);
    }

    // ─── Hilfsfunktionen ───

    private async Task WakeAgentAsync(StagedCall call, string title, string body, CancellationToken ct)
    {
        try
        {
            await _board.CreateAsync(new TaskItem
            {
                Title = title,
                Body = body,
                Status = TaskItemStatus.Todo,
                Assignee = "@" + call.AgentId,
                Type = TaskItemType.Work
            }, ct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Folge-Task für Vorschlag #{Id} konnte nicht angelegt werden", call.Id);
        }
    }

    private async Task RecordAuditAsync(
        StagedCall call, string decidedBy, AuditStatus status, string summary, CancellationToken ct)
    {
        if (_audit is null) return;
        try
        {
            await _audit.AppendAsync(new AuditEntry
            {
                RunId = call.RunId,
                AgentId = call.AgentId,
                Model = "",
                Source = "approval",
                Tool = call.Tool,
                Arguments = Cap(call.ArgumentsJson, 4_000),
                Status = status,
                Summary = Cap(summary, 500),
                OccurredAt = DateTime.Now
            }, ct);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Approval-Record für #{Id} konnte nicht geschrieben werden", call.Id);
        }
    }

    private static string Label(StagedCall call)
        => string.IsNullOrWhiteSpace(call.Action) ? call.Tool : $"{call.Tool}.{call.Action}";

    private static string Cap(string value, int max)
        => value.Length <= max ? value : value[..max] + "…";
}
