using Dapper;
using Kbx.Shared.Experiments.Generated;
using Npgsql;

namespace Kbx.Shared.Experiments;

public sealed class KbxExperimentAssignmentService(NpgsqlDataSource dataSource)
{
    public async Task<KbxExperimentAssignmentsResponse> EvaluateAsync(Guid tenantId,Guid userId,string screenId,CancellationToken ct)
    {
        var definitions=KbxExperimentCatalog.Experiments.Values.Where(x=>x.ScreenId==screenId).ToArray();
        var items=new List<KbxExperimentAssignmentDto>();
        await using var connection=await dataSource.OpenConnectionAsync(ct);
        foreach(var definition in definitions)
        {
            var runtime=await connection.QuerySingleOrDefaultAsync<RuntimeRow>(new CommandDefinition("""
                select state State,rollout_percent RolloutPercent,kill_switch KillSwitch
                  from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@ExperimentId;
            """,new{TenantId=tenantId,ExperimentId=definition.Id},cancellationToken:ct));
            var state=runtime?.State ?? definition.State;
            var rollout=runtime?.RolloutPercent ?? definition.RolloutPercent;
            var killed=runtime?.KillSwitch ?? false;
            // Regular users only receive actively enrolled assignments. Draft/paused/rolled-back definitions stay internal.
            if(killed||state!="running"||rollout<=0)continue;
            // Rollout percentage remains authoritative even for previously assigned users.
            // Assignment is preserved for reproducibility and becomes active again if rollout is raised later.
            if(!KbxExperimentAssignmentPolicy.IsEnrolled(tenantId,userId,definition.Id,rollout))continue;
            var existing=await connection.QuerySingleOrDefaultAsync<string>(new CommandDefinition("""
                select variant from kbx.experiment_assignments where tenant_id=@TenantId and experiment_id=@ExperimentId and user_id=@UserId;
            """,new{TenantId=tenantId,ExperimentId=definition.Id,UserId=userId},cancellationToken:ct));
            if(existing is not null){items.Add(new(definition.Id,definition.FlagId,screenId,true,existing,state));continue;}
            var variant=KbxExperimentAssignmentPolicy.ChooseVariant(tenantId,userId,definition);
            await connection.ExecuteAsync(new CommandDefinition("""
                insert into kbx.experiment_assignments(tenant_id,experiment_id,user_id,variant)
                values(@TenantId,@ExperimentId,@UserId,@Variant) on conflict do nothing;
            """,new{TenantId=tenantId,ExperimentId=definition.Id,UserId=userId,Variant=variant},cancellationToken:ct));
            existing=await connection.QuerySingleAsync<string>(new CommandDefinition("""
                select variant from kbx.experiment_assignments where tenant_id=@TenantId and experiment_id=@ExperimentId and user_id=@UserId;
            """,new{TenantId=tenantId,ExperimentId=definition.Id,UserId=userId},cancellationToken:ct));
            items.Add(new(definition.Id,definition.FlagId,screenId,true,existing,state));
        }
        return new(items);
    }
    private sealed record RuntimeRow(string State,int RolloutPercent,bool KillSwitch);
}
