using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Accounting;

namespace ClawdDotNet.Core.Budget;

/// <summary>Welche Grenze überschritten wurde.</summary>
public enum BudgetLimit
{
    None,
    AgentCost,
    AgentTokens,
    InstanceCost,
    InstanceTokens
}

public sealed record BudgetStatus(bool IsAllowed, BudgetLimit Limit, string? Reason)
{
    public static BudgetStatus Allowed { get; } = new(true, BudgetLimit.None, null);
}

/// <summary>
/// Wacht über das Tagesbudget.
///
/// Hintergrund (K5): Es gab keine Obergrenze. Ein Agent in einer Schleife — etwa durch
/// gegenseitige send_message-Aufrufe — konnte unbeaufsichtigt Guthaben verbrennen; die
/// Credits-Anzeige war rein informativ.
///
/// Zwei Arten von Grenzen, weil sich Kosten nicht immer beziffern lassen: Liefert der
/// Anbieter für ein Modell keine Preise, greift die Kostengrenze nicht — die
/// Token-Grenze dagegen immer. Wer sich absichern will, setzt beide.
/// </summary>
public sealed class BudgetGuard
{
    private readonly IUsageRepository _usage;
    private readonly Func<DateTime> _now;

    public BudgetGuard(IUsageRepository usage, Func<DateTime>? now = null)
    {
        _usage = usage;
        _now = now ?? (() => DateTime.Now);
    }

    /// <summary>
    /// Prüft, ob der Agent heute noch laufen darf.
    /// </summary>
    public async Task<BudgetStatus> CheckAsync(
        AgentConfig agent, InstanceBudget instanceBudget, CancellationToken ct)
    {
        var today = DateOnly.FromDateTime(_now());

        // Agentengrenze
        var agentBudget = agent.Budget;
        if (agentBudget.HasAnyLimit)
        {
            var used = await _usage.GetDailyAsync(today, agent.AgentId, ct);

            if (Exceeds(agentBudget.DailyCostUsd, used.CostUsd))
            {
                return new BudgetStatus(false, BudgetLimit.AgentCost,
                    $"Tagesbudget des Agenten erschöpft: {used.CostUsd:F4} von maximal " +
                    $"{agentBudget.DailyCostUsd:F2} USD verbraucht." +
                    UnpricedHint(used));
            }

            if (Exceeds(agentBudget.DailyTokens, used.TotalTokens))
            {
                return new BudgetStatus(false, BudgetLimit.AgentTokens,
                    $"Token-Tageslimit des Agenten erschöpft: {used.TotalTokens:N0} von maximal " +
                    $"{agentBudget.DailyTokens:N0}.");
            }
        }

        // Instanzgrenze — gilt für alle Agenten zusammen
        if (instanceBudget.HasAnyLimit)
        {
            var used = await _usage.GetDailyAsync(today, agentId: "", ct);

            if (Exceeds(instanceBudget.DailyCostUsd, used.CostUsd))
            {
                return new BudgetStatus(false, BudgetLimit.InstanceCost,
                    $"Tagesbudget der Instanz erschöpft: {used.CostUsd:F4} von maximal " +
                    $"{instanceBudget.DailyCostUsd:F2} USD verbraucht." +
                    UnpricedHint(used));
            }

            if (Exceeds(instanceBudget.DailyTokens, used.TotalTokens))
            {
                return new BudgetStatus(false, BudgetLimit.InstanceTokens,
                    $"Token-Tageslimit der Instanz erschöpft: {used.TotalTokens:N0} von maximal " +
                    $"{instanceBudget.DailyTokens:N0}.");
            }
        }

        return BudgetStatus.Allowed;
    }

    /// <summary>Verbleibendes Kostenbudget des Agenten für heute; null bei fehlender Grenze.</summary>
    public async Task<decimal?> GetRemainingCostAsync(AgentConfig agent, CancellationToken ct)
    {
        if (agent.Budget.DailyCostUsd <= 0)
            return null;

        var used = await _usage.GetDailyAsync(DateOnly.FromDateTime(_now()), agent.AgentId, ct);
        return Math.Max(0m, agent.Budget.DailyCostUsd - used.CostUsd);
    }

    /// <summary>0 oder kleiner bedeutet: keine Grenze gesetzt.</summary>
    private static bool Exceeds(decimal limit, decimal used) => limit > 0 && used >= limit;

    private static bool Exceeds(long limit, long used) => limit > 0 && used >= limit;

    /// <summary>
    /// Ohne Preisangaben ist die verbrauchte Summe unvollständig — sonst wirkte ein
    /// niedriger Wert wie ein noch offener Spielraum.
    /// </summary>
    private static string UnpricedHint(DailyUsage used)
        => used.CostIsComplete
            ? ""
            : " Achtung: Für mindestens ein Modell lagen keine Preise vor, der tatsächliche " +
              "Verbrauch liegt höher.";
}
