using System.Text.Json;
using Dapper;
using Kbx.Shared.Experiments.Generated;
using Npgsql;
namespace Kbx.Shared.Experiments;
public sealed class KbxExperimentRuntimeStore(NpgsqlDataSource dataSource,KbxExperimentRolloutPublisher publisher)
{
    public async Task UpdateRolloutAsync(Guid tenantId,Guid actorId,string experimentId,int percent,string state,string reason,string? correlationId,CancellationToken ct)
    {
        RequireKnownExperiment(experimentId);if(percent is <0 or >100)throw new ArgumentOutOfRangeException(nameof(percent));if(state is not ("running" or "paused"))throw new ArgumentException("state must be running or paused");if(string.IsNullOrWhiteSpace(reason))throw new ArgumentException("reason is required");
        await ChangeAsync(tenantId,actorId,experimentId,"rollout",reason,correlationId,ct,async(connection,tx,before)=>{
            await connection.ExecuteAsync(new CommandDefinition("""
              insert into kbx.experiment_runtime(tenant_id,experiment_id,state,rollout_percent,kill_switch,updated_by)
              values(@TenantId,@ExperimentId,@State,@Percent,false,@Actor)
              on conflict(tenant_id,experiment_id) do update set state=excluded.state,rollout_percent=excluded.rollout_percent,kill_switch=false,version=kbx.experiment_runtime.version+1,updated_by=excluded.updated_by,updated_at=now();
            """,new{TenantId=tenantId,ExperimentId=experimentId,State=state,Percent=percent,Actor=actorId},tx,cancellationToken:ct));
        });
        await publisher.PublishAsync(tenantId,experimentId,ct);
    }
    public async Task RollbackAsync(Guid tenantId,Guid actorId,string experimentId,string reason,string? correlationId,CancellationToken ct)
    {
        RequireKnownExperiment(experimentId);if(string.IsNullOrWhiteSpace(reason))throw new ArgumentException("reason is required");
        await ChangeAsync(tenantId,actorId,experimentId,"rollback",reason,correlationId,ct,async(connection,tx,before)=>{
            await connection.ExecuteAsync(new CommandDefinition("""
              insert into kbx.experiment_runtime(tenant_id,experiment_id,state,rollout_percent,kill_switch,updated_by)
              values(@TenantId,@ExperimentId,'rolled-back',0,true,@Actor)
              on conflict(tenant_id,experiment_id) do update set state='rolled-back',rollout_percent=0,kill_switch=true,version=kbx.experiment_runtime.version+1,updated_by=excluded.updated_by,updated_at=now();
            """,new{TenantId=tenantId,ExperimentId=experimentId,Actor=actorId},tx,cancellationToken:ct));
        });
        await publisher.PublishAsync(tenantId,experimentId,ct);
    }
    private static void RequireKnownExperiment(string experimentId)
    {
        if(!KbxExperimentCatalog.Experiments.ContainsKey(experimentId))throw new ArgumentException($"Unknown experiment: {experimentId}");
    }
    private async Task ChangeAsync(Guid tenantId,Guid actorId,string experimentId,string action,string reason,string? correlationId,CancellationToken ct,Func<NpgsqlConnection,NpgsqlTransaction,string?,Task> mutate)
    {
        await using var connection=await dataSource.OpenConnectionAsync(ct);await using var tx=await connection.BeginTransactionAsync(ct);
        var before=await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("select row_to_json(x)::text from (select state,rollout_percent,kill_switch,version from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@ExperimentId for update) x",new{TenantId=tenantId,ExperimentId=experimentId},tx,cancellationToken:ct));
        await mutate(connection,tx,before);
        var after=await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("select row_to_json(x)::text from (select state,rollout_percent,kill_switch,version from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@ExperimentId) x",new{TenantId=tenantId,ExperimentId=experimentId},tx,cancellationToken:ct));
        await connection.ExecuteAsync(new CommandDefinition("""
          insert into kbx.experiment_audit(id,tenant_id,experiment_id,action,before_state,after_state,actor_id,reason,correlation_id)
          values(@Id,@TenantId,@ExperimentId,@Action,cast(@Before as jsonb),cast(@After as jsonb),@Actor,@Reason,@CorrelationId);
        """,new{Id=Guid.NewGuid(),TenantId=tenantId,ExperimentId=experimentId,Action=action,Before=before,After=after,Actor=actorId,Reason=reason,CorrelationId=correlationId},tx,cancellationToken:ct));
        await tx.CommitAsync(ct);
    }
}
