using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Commerce;
using Tiku.Application.Tenancy;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Tenancy;

internal sealed class TenantExternalProviderConfigService(
    TikuDbContext dbContext,
    ITenantSecretService tenantSecretService) : ITenantExternalProviderConfigService
{
    public async Task<TenantExternalProviderAccount> GetActiveProviderAsync(
        Guid tenantId,
        TenantExternalProviderCapability capability,
        string? provider = null,
        CancellationToken cancellationToken = default)
    {
        var normalizedProvider = NormalizeProvider(provider);
        var query = dbContext.TenantExternalProviders
            .AsNoTracking()
            .Where(item =>
                item.TenantId == tenantId &&
                item.Capability == capability &&
                item.Status == TenantExternalProviderStatus.Active);

        if (!string.IsNullOrWhiteSpace(normalizedProvider))
            query = query.Where(item => item.Provider == normalizedProvider);

        var item = await query
            .OrderBy(item => item.Priority)
            .ThenBy(item => item.Provider)
            .FirstOrDefaultAsync(cancellationToken);

        if (item is null)
            throw new TenantExternalProviderException(
                "Tenant external provider is not configured.",
                "tenant_external_provider_not_configured");

        var secretPayload = string.IsNullOrWhiteSpace(item.SecretRef)
            ? JsonDocument.Parse("{}").RootElement.Clone()
            : await tenantSecretService.GetActiveSecretPayloadAsync(
                tenantId,
                item.SecretRef,
                cancellationToken);

        return new TenantExternalProviderAccount(
            item.TenantId,
            item.Capability,
            item.Provider,
            item.Status,
            item.DisplayName,
            item.ConfigPublic,
            secretPayload,
            item.SecretRef,
            item.Priority,
            item.Metadata);
    }

    public async Task<IReadOnlyCollection<TenantExternalProviderItem>> GetProvidersAsync(
        Guid tenantId,
        TenantExternalProviderCapability? capability = null,
        string? provider = null,
        int? limit = null,
        CancellationToken cancellationToken = default)
    {
        var normalizedProvider = NormalizeProvider(provider);
        var query = dbContext.TenantExternalProviders
            .AsNoTracking()
            .Where(item => item.TenantId == tenantId);

        if (capability.HasValue) query = query.Where(item => item.Capability == capability.Value);

        if (!string.IsNullOrWhiteSpace(normalizedProvider))
            query = query.Where(item => item.Provider == normalizedProvider);

        return await query
            .OrderBy(item => item.Capability)
            .ThenBy(item => item.Priority)
            .ThenBy(item => item.Provider)
            .Take(Math.Clamp(limit ?? 100, 1, 200))
            .Select(item => ToItem(item))
            .ToArrayAsync(cancellationToken);
    }

    public async Task<TenantExternalProviderItem> UpsertProviderAsync(
        Guid tenantId,
        UpsertTenantExternalProviderCommand command,
        CancellationToken cancellationToken = default)
    {
        var provider = NormalizeRequiredProvider(command.Provider);
        AssertPublicConfig(command.ConfigPublic, "config_public");
        AssertPublicConfig(command.Metadata, "metadata");

        var item = await dbContext.TenantExternalProviders
            .SingleOrDefaultAsync(
                providerConfig =>
                    providerConfig.TenantId == tenantId &&
                    providerConfig.Capability == command.Capability &&
                    providerConfig.Provider == provider,
                cancellationToken);

        if (item is null)
        {
            item = new TenantExternalProvider
            {
                TenantId = tenantId,
                Capability = command.Capability,
                Provider = provider
            };
            dbContext.TenantExternalProviders.Add(item);
        }

        item.Status = command.Status;
        item.DisplayName = command.DisplayName?.Trim();
        item.SecretRef = string.IsNullOrWhiteSpace(command.SecretRef) ? null : command.SecretRef.Trim();
        item.Priority = Math.Clamp(command.Priority ?? item.Priority, 0, 10_000);
        item.ConfigPublic = JsonObjectOrDefault(command.ConfigPublic);
        item.Metadata = JsonObjectOrDefault(command.Metadata);

        await dbContext.SaveChangesAsync(cancellationToken);
        return ToItem(item);
    }

    public static string NormalizeRequiredProvider(string provider)
    {
        var normalized = NormalizeProvider(provider);
        if (string.IsNullOrWhiteSpace(normalized))
            throw new TenantExternalProviderException("Provider is required.", "provider_required");

        return normalized;
    }

    public static string? NormalizeProvider(string? provider)
    {
        var normalized = (provider ?? string.Empty).Trim().ToLowerInvariant()
            .Replace("-", "_", StringComparison.Ordinal);
        return normalized switch
        {
            "wechat" or "wechatpay" or "wxpay" or "wx_pay" => "wechat_pay",
            "ali_pay" => "alipay",
            "" => null,
            _ => normalized
        };
    }

    private static TenantExternalProviderItem ToItem(TenantExternalProvider item)
    {
        return new TenantExternalProviderItem(
            item.Id,
            item.Capability,
            item.Provider,
            item.Status,
            item.DisplayName,
            item.SecretRef,
            item.Priority,
            item.ConfigPublic,
            item.Metadata,
            item.CreatedAt,
            item.UpdatedAt);
    }

    private static JsonElement JsonObjectOrDefault(JsonElement element)
    {
        return element.ValueKind == JsonValueKind.Object
            ? element.Clone()
            : JsonSerializer.SerializeToElement(new { });
    }

    private static void AssertPublicConfig(JsonElement element, string path)
    {
        if (element.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) return;

        if (element.ValueKind != JsonValueKind.Object)
            throw new TenantExternalProviderException(
                $"{path} must be a JSON object.",
                "provider_config_must_be_object");

        foreach (var property in element.EnumerateObject())
        {
            var key = property.Name.Replace("_", string.Empty, StringComparison.Ordinal).ToLowerInvariant();
            if (key.Contains("secret", StringComparison.Ordinal) ||
                key.Contains("token", StringComparison.Ordinal) ||
                key.Contains("privatekey", StringComparison.Ordinal) ||
                key is "key" or "apikey" or "accesskey" or "accesskeyid" or "accesskeysecret" or "appsecret"
                    or "apiv3key")
                throw new TenantExternalProviderException(
                    $"{path} cannot contain secrets.",
                    "public_config_contains_secret");

            if (property.Value.ValueKind == JsonValueKind.Object)
                AssertPublicConfig(property.Value, $"{path}.{property.Name}");
        }
    }
}