namespace Nexus.Api.Services;

public sealed class AgentProvisioningOptions
{
    public const string SectionName = "OpenClawAgentProvisioning";

    /// <summary>
    /// OpenClaw-host path, not a Nexus container mount. The Gateway resolves
    /// the leading tilde when agents.create is executed.
    /// </summary>
    public string WorkspaceRoot { get; set; } = "~/.openclaw";

    public int PollIntervalSeconds { get; set; } = 5;
    public int LeaseSeconds { get; set; } = 60;
    public int MaxProposalFileBytes { get; set; } = 262_144;
    public int MaxProposalFilesBytes { get; set; } = 524_288;
}

/// <summary>
/// Bounded wake-up signal. PostgreSQL remains authoritative; losing this
/// process-local signal merely falls back to polling.
/// </summary>
public sealed class AgentProvisioningSignal
{
    private readonly System.Threading.Channels.Channel<bool> channel =
        System.Threading.Channels.Channel.CreateBounded<bool>(
            new System.Threading.Channels.BoundedChannelOptions(1)
            {
                FullMode = System.Threading.Channels.BoundedChannelFullMode.DropWrite,
                SingleReader = true,
                SingleWriter = false
            });

    public void Notify() => channel.Writer.TryWrite(true);

    public async Task WaitAsync(TimeSpan maximumDelay, CancellationToken cancellationToken)
    {
        using var delayCancellation = CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken);
        var signal = channel.Reader.WaitToReadAsync(cancellationToken).AsTask();
        var delay = Task.Delay(maximumDelay, delayCancellation.Token);
        var completed = await Task.WhenAny(signal, delay);
        if (completed == signal && await signal)
        {
            while (channel.Reader.TryRead(out _))
            {
            }
        }

        delayCancellation.Cancel();
    }
}

public sealed class AgentProvisioningWorker(
    IServiceScopeFactory scopeFactory,
    AgentProvisioningSignal signal,
    Microsoft.Extensions.Options.IOptions<AgentProvisioningOptions> options,
    ILogger<AgentProvisioningWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using (var startupScope = scopeFactory.CreateAsyncScope())
        {
            try
            {
                await startupScope.ServiceProvider
                    .GetRequiredService<IAgentProposalService>()
                    .RecoverInterruptedRequestsAsync(stoppingToken);
            }
            catch (Exception exception) when (!stoppingToken.IsCancellationRequested)
            {
                logger.LogError(
                    exception,
                    "Agent provisioning recovery failed; live mutations remain paused until the next poll");
            }
        }

        var delay = TimeSpan.FromSeconds(Math.Clamp(
            options.Value.PollIntervalSeconds,
            1,
            60));
        while (!stoppingToken.IsCancellationRequested)
        {
            var processed = false;
            try
            {
                await using var scope = scopeFactory.CreateAsyncScope();
                processed = await scope.ServiceProvider
                    .GetRequiredService<IAgentProposalService>()
                    .ProcessNextAsync(stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception exception)
            {
                logger.LogError(exception, "Agent provisioning worker iteration failed");
            }

            if (!processed)
                await signal.WaitAsync(delay, stoppingToken);
        }
    }
}
