using System.Diagnostics;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Tiku.Application.Security;
using Tiku.Domain.Operations;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Tenancy;

public sealed class TenantExecutionScope(
    IServiceScopeFactory scopeFactory,
    ILogger<TenantExecutionScope> logger) : ITenantExecutionScope
{
    public Task ExecuteAsync(
        SystemScopeRequest request,
        Func<IServiceProvider, CancellationToken, Task> operation,
        CancellationToken cancellationToken = default)
    {
        return ExecuteAsync<object?>(request, async (provider, token) =>
        {
            await operation(provider, token);
            return null;
        }, cancellationToken);
    }

    public async Task<TResult> ExecuteAsync<TResult>(
        SystemScopeRequest request,
        Func<IServiceProvider, CancellationToken, Task<TResult>> operation,
        CancellationToken cancellationToken = default)
    {
        Validate(request, operation);
        await using var scope = scopeFactory.CreateAsyncScope();
        var initializer = scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>();
        initializer.InitializeSystem(request.TargetTenantId, request.Reason);
        logger.LogWarning(
            "Entering audited system scope. CallerType={CallerType} Caller={Caller} TargetTenantId={TargetTenantId} CorrelationId={CorrelationId} Reason={Reason}",
            request.CallerType,
            request.Caller,
            request.TargetTenantId,
            request.CorrelationId,
            request.Reason);
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var started = DateTimeOffset.UtcNow;
        var stopwatch = Stopwatch.StartNew();
        await using var transaction = dbContext.Database.IsRelational()
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        await WriteAuditAsync(dbContext, request, "system_scope.entered", started, null, null, cancellationToken);
        try
        {
            var result = await operation(scope.ServiceProvider, cancellationToken);
            await WriteAuditAsync(
                dbContext, request, "system_scope.completed", started, stopwatch.ElapsedMilliseconds, null,
                cancellationToken);
            if (transaction is not null) await transaction.CommitAsync(cancellationToken);
            return result;
        }
        catch (Exception exception)
        {
            if (transaction is not null)
            {
                await transaction.RollbackAsync(CancellationToken.None);
                dbContext.ChangeTracker.Clear();
                await WriteAuditAsync(
                    dbContext, request, "system_scope.entered", started, null, null, CancellationToken.None);
            }

            await WriteAuditAsync(
                dbContext, request, "system_scope.failed", started, stopwatch.ElapsedMilliseconds,
                exception.GetType().Name, CancellationToken.None);
            throw;
        }
    }

    private static void Validate<TResult>(
        SystemScopeRequest request,
        Func<IServiceProvider, CancellationToken, Task<TResult>> operation)
    {
        ArgumentNullException.ThrowIfNull(request);
        ArgumentNullException.ThrowIfNull(operation);
        ArgumentException.ThrowIfNullOrWhiteSpace(request.Caller);
        ArgumentException.ThrowIfNullOrWhiteSpace(request.Reason);
        ArgumentException.ThrowIfNullOrWhiteSpace(request.CorrelationId);
        if (request.TargetTenantId is null && !request.IsGlobal)
            throw new ArgumentException("System scope requires a target tenant or an explicit global declaration.",
                nameof(request));
        if (request.TargetTenantId is not null && request.IsGlobal)
            throw new ArgumentException("A tenant-targeted system scope cannot also be global.", nameof(request));
        if (request.IsGlobal &&
            request.CallerType is SystemScopeCallerType.Worker or SystemScopeCallerType.PublicQuestionBank)
            throw new ArgumentException("Worker and public question bank scopes must target a tenant.",
                nameof(request));
        if (!Enum.IsDefined(request.CallerType))
            throw new ArgumentOutOfRangeException(nameof(request), "Unknown system scope caller type.");
    }

    private static async Task WriteAuditAsync(
        TikuDbContext dbContext,
        SystemScopeRequest request,
        string action,
        DateTimeOffset startedAt,
        long? elapsedMilliseconds,
        string? failureType,
        CancellationToken cancellationToken)
    {
        dbContext.AuditLogs.Add(new AuditLog
        {
            TenantId = request.TargetTenantId,
            Action = action,
            TargetType = "system_scope",
            TargetId = request.CorrelationId,
            Details = JsonSerializer.SerializeToElement(new
            {
                callerType = request.CallerType.ToString(),
                request.Caller,
                request.Reason,
                request.CorrelationId,
                request.IsGlobal,
                startedAt,
                elapsedMilliseconds,
                failureType
            })
        });
        await dbContext.SaveChangesAsync(cancellationToken);
    }
}