using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Backoffice;
using Tiku.Application.PlatformAdmin;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.PlatformAdmin;

internal sealed class PlatformApprovalService(
    TikuDbContext dbContext,
    IServiceScopeFactory scopeFactory,
    ITenantExecutionScope tenantExecutionScope,
    IOperationAuditService auditService,
    IPlatformBillingAdminService billingService,
    IPlatformAdminService platformAdminService,
    IPlatformPaymentSettingsService paymentSettingsService,
    IBackofficeService backofficeService) : IPlatformApprovalService
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        Converters = { new JsonStringEnumConverter() }
    };

    public async Task<IReadOnlyCollection<PlatformApprovalRequestItem>> ListAsync(
        PlatformApprovalActor actor,
        PlatformApprovalRequestStatus? status,
        int limit,
        CancellationToken cancellationToken = default)
    {
        Require(actor, BackendPermissions.PlatformApprovalView);
        await ExpirePendingAsync(cancellationToken);
        var query = dbContext.PlatformApprovalRequests.AsNoTracking();
        if (status.HasValue) query = query.Where(item => item.Status == status.Value);
        return await query.OrderByDescending(item => item.CreatedAt)
            .Take(Math.Clamp(limit, 1, 500))
            .Select(item => ToItem(item))
            .ToArrayAsync(cancellationToken);
    }

    public async Task<PlatformApprovalRequestItem> GetAsync(
        PlatformApprovalActor actor,
        Guid requestId,
        CancellationToken cancellationToken = default)
    {
        Require(actor, BackendPermissions.PlatformApprovalView);
        var item = await RequiredRequestAsync(requestId, cancellationToken);
        if (item.Status == PlatformApprovalRequestStatus.Pending && item.ExpiresAt <= DateTimeOffset.UtcNow)
        {
            item.Status = PlatformApprovalRequestStatus.Expired;
            item.ConcurrencyStamp = Guid.NewGuid();
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return ToItem(item);
    }

    public async Task<IReadOnlyCollection<PlatformApprovalPolicyItem>> ListPoliciesAsync(
        PlatformApprovalActor actor,
        CancellationToken cancellationToken = default)
    {
        Require(actor, BackendPermissions.PlatformApprovalView);
        return await dbContext.PlatformApprovalPolicies.AsNoTracking()
            .OrderBy(item => item.Code)
            .Select(item => ToItem(item))
            .ToArrayAsync(cancellationToken);
    }

    public async Task<PlatformApprovalPolicyItem> UpdatePolicyAsync(
        PlatformApprovalActor actor,
        UpdatePlatformApprovalPolicyCommand command,
        CancellationToken cancellationToken = default)
    {
        Require(actor, BackendPermissions.PlatformApprovalPolicyManage);
        var policy =
            await dbContext.PlatformApprovalPolicies.SingleOrDefaultAsync(item => item.Code == command.Code,
                cancellationToken)
            ?? throw Error("Approval policy was not found.", "approval_policy_not_found");
        if (command.ExpiresAfterHours is < 1 or > 720 || command.AmountThresholdCents is <= 0)
            throw Error("Approval policy limits are invalid.", "approval_policy_invalid");
        policy.Enabled = command.Enabled;
        policy.AlwaysRequireApproval = command.AlwaysRequireApproval;
        policy.AmountThresholdCents = command.AmountThresholdCents;
        policy.ExpiresAfterHours = command.ExpiresAfterHours;
        policy.Conditions = command.Conditions.Clone();
        policy.Version++;
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor.UserId, "platform.approval_policy.updated", "platform_approval_policies", policy.Id,
            new { policy.Code, policy.Version }, cancellationToken);
        return ToItem(policy);
    }

    public Task<PlatformApprovalRequestItem> ApproveAsync(PlatformApprovalActor actor, Guid requestId, string reason,
        CancellationToken cancellationToken = default)
    {
        return DecideAsync(actor, requestId, true, reason, cancellationToken);
    }

    public Task<PlatformApprovalRequestItem> RejectAsync(PlatformApprovalActor actor, Guid requestId, string reason,
        CancellationToken cancellationToken = default)
    {
        return DecideAsync(actor, requestId, false, reason, cancellationToken);
    }

    public async Task<PlatformApprovalRequestItem> CancelAsync(
        PlatformApprovalActor actor,
        Guid requestId,
        string reason,
        CancellationToken cancellationToken = default)
    {
        var item = await RequiredRequestAsync(requestId, cancellationToken);
        if (item.RequestedBy != actor.UserId)
            throw Error("Only the requester can cancel an approval request.", "approval_cancel_denied");
        EnsurePending(item);
        item.Status = PlatformApprovalRequestStatus.Cancelled;
        item.DecisionReason = RequiredReason(reason);
        item.DecidedAt = DateTimeOffset.UtcNow;
        item.ConcurrencyStamp = Guid.NewGuid();
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor.UserId, "platform.approval.cancelled", "platform_approval_requests", item.Id,
            new { item.RequestNo }, cancellationToken);
        return ToItem(item);
    }

    public Task<PlatformCommandSubmission> SubmitRefundAsync(
        SaasCatalogActor actor,
        RequestPlatformRefundCommand command,
        CancellationToken cancellationToken = default)
    {
        return SubmitAsync(actor.UserId, PlatformApprovalPolicyCodes.FinancialAdjustment,
            nameof(RequestPlatformRefundCommand), "platform_billing_payments", command.PaymentId.ToString("N"),
            command.AmountCents, command.IdempotencyKey, command.Reason, command,
            async token => await billingService.RequestRefundAsync(actor, command, token), cancellationToken);
    }

    public async Task<PlatformCommandSubmission> ConfirmManualPaymentAsync(
        SaasCatalogActor actor,
        ConfirmManualPaymentCommand command,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
    {
        var amount = await tenantExecutionScope.ExecuteAsync(
                         new SystemScopeRequest(
                             null,
                             SystemScopeCallerType.Platform,
                             nameof(PlatformApprovalService),
                             "Resolve payment amount for platform approval",
                             command.PaymentId.ToString("N"),
                             true),
                         async (services, token) => await services.GetRequiredService<TikuDbContext>()
                             .PlatformBillingPayments.AsNoTracking()
                             .Where(item => item.Id == command.PaymentId)
                             .Select(item => (int?)item.AmountCents)
                             .SingleOrDefaultAsync(token),
                         cancellationToken)
                     ?? throw Error("Platform billing payment was not found.", "platform_billing_payment_not_found");
        return await SubmitAsync(actor.UserId, PlatformApprovalPolicyCodes.FinancialAdjustment,
            nameof(ConfirmManualPaymentCommand), "platform_billing_payments", command.PaymentId.ToString("N"),
            amount, idempotencyKey, command.Reason, command,
            async token => await billingService.ConfirmManualPaymentAsync(actor, command, token), cancellationToken);
    }

    public Task<PlatformCommandSubmission> UpdateTenantStatusAsync(
        PlatformAdminActor actor,
        UpdatePlatformTenantStatusCommand command,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
    {
        if (command.Status != TenantStatus.Archived)
            return ExecuteImmediateAsync(() =>
                platformAdminService.UpdateTenantStatusAsync(actor, command, cancellationToken));
        return SubmitAsync(actor.UserId, PlatformApprovalPolicyCodes.TenantArchive,
            nameof(UpdatePlatformTenantStatusCommand), "tenants", command.TenantId.ToString("N"), null,
            idempotencyKey, command.Reason, command,
            async token => await platformAdminService.UpdateTenantStatusAsync(actor, command, token),
            cancellationToken);
    }

    public Task<PlatformCommandSubmission> UpsertPaymentChannelAsync(
        PlatformCapabilityActor actor,
        UpsertPlatformPaymentChannelCommand command,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
    {
        return SubmitAsync(actor.UserId, PlatformApprovalPolicyCodes.PaymentChannelChange,
            nameof(UpsertPlatformPaymentChannelCommand), "platform_payment_channels",
            command.Id?.ToString("N") ?? command.Provider,
            null, idempotencyKey, "支付渠道或密钥引用变更", command,
            async token => await paymentSettingsService.UpsertChannelAsync(actor, command, token), cancellationToken);
    }

    public async Task<PlatformCommandSubmission> ReplaceRoleBindingsAsync(
        BackofficeActor actor,
        ReplaceRoleBindingsCommand command,
        string idempotencyKey,
        CancellationToken cancellationToken = default)
    {
        var isSuperAdmin = await dbContext.PlatformBackendRoles.AsNoTracking()
            .AnyAsync(role => role.Id == command.RoleId && role.Code == "platform_super_admin", cancellationToken);
        if (!isSuperAdmin)
            return await ExecuteImmediateAsync(() =>
                backofficeService.ReplacePlatformRoleBindingsAsync(actor, command, cancellationToken));
        return await SubmitAsync(actor.UserId, PlatformApprovalPolicyCodes.SuperAdminGrant,
            nameof(ReplaceRoleBindingsCommand), "platform_backend_roles", command.RoleId.ToString("N"), null,
            idempotencyKey, "平台超级管理员权限变更", command,
            async token => await backofficeService.ReplacePlatformRoleBindingsAsync(actor, command, token),
            cancellationToken);
    }

    public async Task<int> ProcessApprovedAsync(int batchSize = 20, CancellationToken cancellationToken = default)
    {
        var requestIds = await dbContext.PlatformApprovalRequests.AsNoTracking()
            .Where(item => item.Status == PlatformApprovalRequestStatus.Approved)
            .OrderBy(item => item.DecidedAt)
            .Select(item => item.Id)
            .Take(Math.Clamp(batchSize, 1, 100))
            .ToArrayAsync(cancellationToken);
        var processed = 0;
        foreach (var requestId in requestIds)
        {
            var claimed = await dbContext.PlatformApprovalRequests
                .Where(item => item.Id == requestId && item.Status == PlatformApprovalRequestStatus.Approved)
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(item => item.Status, PlatformApprovalRequestStatus.Executing)
                    .SetProperty(item => item.ConcurrencyStamp, Guid.NewGuid())
                    .SetProperty(item => item.UpdatedAt, DateTimeOffset.UtcNow), cancellationToken);
            if (claimed == 0) continue;

            dbContext.ChangeTracker.Clear();
            var item = await RequiredRequestAsync(requestId, cancellationToken);
            try
            {
                item.ResultSnapshot = await ExecuteApprovedAsync(item, cancellationToken);
                item.Status = PlatformApprovalRequestStatus.Succeeded;
                item.ExecutedAt = DateTimeOffset.UtcNow;
                item.Error = null;
            }
            catch (Exception exception) when (exception is not OperationCanceledException)
            {
                item.Status = PlatformApprovalRequestStatus.Failed;
                item.Error = exception.Message.Length > 4000 ? exception.Message[..4000] : exception.Message;
            }

            item.ConcurrencyStamp = Guid.NewGuid();
            await dbContext.SaveChangesAsync(cancellationToken);
            await AuditAsync(item.DecidedBy ?? item.RequestedBy,
                item.Status == PlatformApprovalRequestStatus.Succeeded
                    ? "platform.approval.executed"
                    : "platform.approval.execution_failed",
                "platform_approval_requests", item.Id, new { item.RequestNo, item.CommandType, item.Error },
                cancellationToken);
            processed++;
        }

        return processed;
    }

    private async Task<PlatformCommandSubmission> SubmitAsync<TCommand, TResult>(
        Guid actorUserId,
        string policyCode,
        string commandType,
        string targetType,
        string targetId,
        int? amountCents,
        string idempotencyKey,
        string? reason,
        TCommand command,
        Func<CancellationToken, Task<TResult>> execute,
        CancellationToken cancellationToken)
    {
        idempotencyKey = string.IsNullOrWhiteSpace(idempotencyKey)
            ? throw Error("Idempotency-Key is required.", "idempotency_key_required")
            : idempotencyKey.Trim();
        var policy = await dbContext.PlatformApprovalPolicies.AsNoTracking()
                         .SingleOrDefaultAsync(item => item.Code == policyCode, cancellationToken)
                     ?? throw Error("Approval policy is not configured.", "approval_policy_not_configured");
        var requiresApproval = PlatformApprovalRules.RequiresApproval(
            policy.Enabled, policy.AlwaysRequireApproval, policy.AmountThresholdCents, amountCents);
        if (!requiresApproval)
            return await ExecuteImmediateAsync(() => execute(cancellationToken));

        var snapshot = RedactedSnapshot(command);
        var requestHash = Hash(snapshot.GetRawText());
        var existing = await dbContext.PlatformApprovalRequests.AsNoTracking().SingleOrDefaultAsync(item =>
                item.RequestedBy == actorUserId && item.CommandType == commandType &&
                item.IdempotencyKey == idempotencyKey,
            cancellationToken);
        if (existing is not null)
        {
            if (!string.Equals(existing.RequestHash, requestHash, StringComparison.Ordinal))
                throw Error("Idempotency key was used with a different approval request.", "idempotency_conflict");
            return new PlatformCommandSubmission(
                existing.Status == PlatformApprovalRequestStatus.Succeeded ? "executed" : "pending_approval",
                existing.ResultSnapshot, ToItem(existing));
        }

        var item = new PlatformApprovalRequest
        {
            RequestNo = $"PA{DateTimeOffset.UtcNow:yyyyMMddHHmmss}{Guid.NewGuid():N}"[..24],
            PolicyCode = policy.Code,
            PolicyVersion = policy.Version,
            RequestedBy = actorUserId,
            RequiredPermission = policy.RequiredPermission,
            CommandType = commandType,
            TargetType = targetType,
            TargetId = targetId,
            AmountCents = amountCents,
            IdempotencyKey = idempotencyKey,
            RequestHash = requestHash,
            RequestSnapshot = snapshot,
            RequestReason = reason?.Trim(),
            ExpiresAt = DateTimeOffset.UtcNow.AddHours(policy.ExpiresAfterHours)
        };
        dbContext.PlatformApprovalRequests.Add(item);
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actorUserId, "platform.approval.requested", "platform_approval_requests", item.Id,
            new { item.RequestNo, item.PolicyCode, item.CommandType, item.TargetType, item.TargetId, item.AmountCents },
            cancellationToken);
        return new PlatformCommandSubmission("pending_approval", null, ToItem(item));
    }

    private async Task<PlatformApprovalRequestItem> DecideAsync(
        PlatformApprovalActor actor,
        Guid requestId,
        bool approve,
        string reason,
        CancellationToken cancellationToken)
    {
        Require(actor, BackendPermissions.PlatformApprovalDecide);
        var item = await RequiredRequestAsync(requestId, cancellationToken);
        var denial = PlatformApprovalRules.DecisionDenialCode(item.Status, item.RequestedBy, actor.UserId,
            item.ExpiresAt, item.RequiredPermission, actor.Permissions, DateTimeOffset.UtcNow);
        if (denial == "approval_request_expired")
        {
            item.Status = PlatformApprovalRequestStatus.Expired;
            item.ConcurrencyStamp = Guid.NewGuid();
            await dbContext.SaveChangesAsync(cancellationToken);
            throw Error("Approval request has expired.", "approval_request_expired");
        }

        if (denial == "approval_request_not_pending")
            throw Error("Only a pending approval request can be changed.", denial);
        if (denial == "approval_maker_checker_required")
            throw Error("Requester cannot approve or reject the same request.", "approval_maker_checker_required");
        if (denial == "approval_business_permission_required")
            throw Error("Approver no longer has the required business permission.",
                "approval_business_permission_required");

        item.DecidedBy = actor.UserId;
        item.DecidedAt = DateTimeOffset.UtcNow;
        item.DecisionReason = RequiredReason(reason);
        item.Status = approve ? PlatformApprovalRequestStatus.Approved : PlatformApprovalRequestStatus.Rejected;
        item.ConcurrencyStamp = Guid.NewGuid();
        await dbContext.SaveChangesAsync(cancellationToken);
        await AuditAsync(actor.UserId, approve ? "platform.approval.approved" : "platform.approval.rejected",
            "platform_approval_requests", item.Id, new { item.RequestNo, item.PolicyCode }, cancellationToken);
        return ToItem(item);
    }

    private async Task<JsonElement> ExecuteApprovedAsync(PlatformApprovalRequest item,
        CancellationToken cancellationToken)
    {
        await using var scope = scopeFactory.CreateAsyncScope();
        scope.ServiceProvider.GetRequiredService<ICurrentUser>().Load(new ClaimsPrincipal(
            new ClaimsIdentity(
                [new Claim(TikuClaimTypes.UserId, item.RequestedBy.ToString())],
                "platform-approval")));
        object result = item.CommandType switch
        {
            nameof(RequestPlatformRefundCommand) => await scope.ServiceProvider
                .GetRequiredService<IPlatformBillingAdminService>()
                .RequestRefundAsync(new SaasCatalogActor(item.RequestedBy),
                    Deserialize<RequestPlatformRefundCommand>(item), cancellationToken),
            nameof(ConfirmManualPaymentCommand) => await scope.ServiceProvider
                .GetRequiredService<IPlatformBillingAdminService>()
                .ConfirmManualPaymentAsync(new SaasCatalogActor(item.RequestedBy),
                    Deserialize<ConfirmManualPaymentCommand>(item), cancellationToken),
            nameof(UpdatePlatformTenantStatusCommand) => await scope.ServiceProvider
                .GetRequiredService<IPlatformAdminService>()
                .UpdateTenantStatusAsync(new PlatformAdminActor(item.RequestedBy),
                    Deserialize<UpdatePlatformTenantStatusCommand>(item), cancellationToken),
            nameof(UpsertPlatformPaymentChannelCommand) => await scope.ServiceProvider
                .GetRequiredService<IPlatformPaymentSettingsService>()
                .UpsertChannelAsync(new PlatformCapabilityActor(item.RequestedBy),
                    Deserialize<UpsertPlatformPaymentChannelCommand>(item), cancellationToken),
            nameof(ReplaceRoleBindingsCommand) => await scope.ServiceProvider.GetRequiredService<IBackofficeService>()
                .ReplacePlatformRoleBindingsAsync(new BackofficeActor(item.RequestedBy, null, true),
                    Deserialize<ReplaceRoleBindingsCommand>(item), cancellationToken),
            _ => throw Error("Approval command type is not supported.", "approval_command_not_supported")
        };
        return JsonSerializer.SerializeToElement(result, JsonOptions);
    }

    private static async Task<PlatformCommandSubmission> ExecuteImmediateAsync<TResult>(Func<Task<TResult>> execute)
    {
        var result = await execute();
        return new PlatformCommandSubmission("executed", JsonSerializer.SerializeToElement(result, JsonOptions), null);
    }

    private static T Deserialize<T>(PlatformApprovalRequest item)
    {
        return JsonSerializer.Deserialize<T>(item.RequestSnapshot.GetRawText(), JsonOptions)
               ?? throw Error("Approval request snapshot is invalid.", "approval_snapshot_invalid");
    }

    private async Task<PlatformApprovalRequest> RequiredRequestAsync(Guid requestId,
        CancellationToken cancellationToken)
    {
        return await dbContext.PlatformApprovalRequests.SingleOrDefaultAsync(item => item.Id == requestId,
                   cancellationToken)
               ?? throw Error("Approval request was not found.", "approval_request_not_found");
    }

    private async Task ExpirePendingAsync(CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        await dbContext.PlatformApprovalRequests
            .Where(item => item.Status == PlatformApprovalRequestStatus.Pending && item.ExpiresAt <= now)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.Status, PlatformApprovalRequestStatus.Expired)
                .SetProperty(item => item.ConcurrencyStamp, Guid.NewGuid())
                .SetProperty(item => item.UpdatedAt, now), cancellationToken);
    }

    private Task AuditAsync(Guid actor, string action, string targetType, Guid targetId, object details,
        CancellationToken cancellationToken)
    {
        return auditService.WriteAsync(
            new BackofficeOperationAuditCommand(null, actor, action, targetType, targetId.ToString("N"),
                JsonSerializer.SerializeToElement(details, JsonOptions)), cancellationToken);
    }

    private static JsonElement RedactedSnapshot<T>(T command)
    {
        var node = JsonSerializer.SerializeToNode(command, JsonOptions) ?? new JsonObject();
        Redact(node);
        return JsonSerializer.SerializeToElement(node, JsonOptions);
    }

    private static void Redact(JsonNode? node)
    {
        if (node is JsonObject value)
            foreach (var property in value.ToArray())
            {
                var name = property.Key;
                if ((name.Contains("password", StringComparison.OrdinalIgnoreCase) ||
                     name.Contains("token", StringComparison.OrdinalIgnoreCase) ||
                     name.Equals("secret", StringComparison.OrdinalIgnoreCase)) &&
                    !name.EndsWith("Ref", StringComparison.OrdinalIgnoreCase))
                    value[name] = "***";
                else Redact(property.Value);
            }
        else if (node is JsonArray array)
            foreach (var item in array)
                Redact(item);
    }

    private static string Hash(string value)
    {
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
    }

    private static string RequiredReason(string reason)
    {
        return string.IsNullOrWhiteSpace(reason)
            ? throw Error("Decision reason is required.", "approval_reason_required")
            : reason.Trim();
    }

    private static void EnsurePending(PlatformApprovalRequest item)
    {
        if (item.Status != PlatformApprovalRequestStatus.Pending)
            throw Error("Only a pending approval request can be changed.", "approval_request_not_pending");
    }

    private static void Require(PlatformApprovalActor actor, string permission)
    {
        if (!actor.Permissions.Contains(permission))
            throw Error("Platform approval access is denied.", "platform_access_denied");
    }

    private static PlatformApprovalException Error(string message, string code)
    {
        return new PlatformApprovalException(message, code);
    }

    private static PlatformApprovalRequestItem ToItem(PlatformApprovalRequest item)
    {
        return new PlatformApprovalRequestItem(item.Id, item.RequestNo, item.PolicyCode, item.PolicyVersion,
            item.Status, item.RequestedBy, item.DecidedBy, item.RequiredPermission, item.CommandType, item.TargetType,
            item.TargetId, item.AmountCents,
            item.RequestSnapshot, item.RequestReason, item.DecisionReason, item.Error, item.ExpiresAt, item.CreatedAt,
            item.UpdatedAt);
    }

    private static PlatformApprovalPolicyItem ToItem(PlatformApprovalPolicy item)
    {
        return new PlatformApprovalPolicyItem(item.Id, item.Code, item.Name, item.RequiredPermission,
            item.Enabled, item.AlwaysRequireApproval, item.AmountThresholdCents, item.Version, item.ExpiresAfterHours,
            item.Conditions);
    }
}