using Npgsql;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Tenancy;
using Tiku.Domain.Tenancy;

namespace Tiku.Infrastructure.Tenancy;

public sealed class TenantDirectory(
    NpgsqlDataSource dataSource,
    IMemoryCache memoryCache,
    IServiceProvider serviceProvider) : ITenantDirectory
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
    private sealed record CacheEnvelope(bool Found, TenantDirectoryEntry? Entry);

    public Task<TenantDirectoryEntry?> FindByHostAsync(
        string host,
        CancellationToken cancellationToken = default)
    {
        const string sql = """
            select t.id, t.slug, t.name, t.status, t.mode, d.host
            from tenant_domains d
            join tenants t on t.id = d.tenant_id
            where d.host = @lookup
              and d.status = 'active'
              and t.status = 'active'
            limit 1
            """;
        return FindAsync(sql, "host", Normalize(host), cancellationToken);
    }

    public Task<TenantDirectoryEntry?> FindByCodeAsync(
        string tenantCode,
        CancellationToken cancellationToken = default)
    {
        const string sql = """
            select t.id, t.slug, t.name, t.status, t.mode, null::text as host
            from tenants t
            where t.slug = @lookup
              and t.status = 'active'
            limit 1
            """;
        return FindAsync(sql, "code", Normalize(tenantCode), cancellationToken);
    }

    private async Task<TenantDirectoryEntry?> FindAsync(
        string sql,
        string kind,
        string lookup,
        CancellationToken cancellationToken)
    {
        var cacheKey = $"tenant-directory:v1:{kind}:{lookup}";
        if (memoryCache.TryGetValue<CacheEnvelope>(cacheKey, out var memoryValue) && memoryValue is not null)
        {
            return memoryValue.Entry;
        }

        var distributedCache = serviceProvider.GetService<IDistributedCache>();
        if (distributedCache is not null)
        {
            try
            {
                var json = await distributedCache.GetStringAsync(cacheKey, cancellationToken);
                if (json is not null)
                {
                    var distributedValue = JsonSerializer.Deserialize<CacheEnvelope>(json, SerializerOptions);
                    if (distributedValue is not null)
                    {
                        memoryCache.Set(cacheKey, distributedValue, distributedValue.Found
                            ? TimeSpan.FromSeconds(30)
                            : TimeSpan.FromSeconds(20));
                        return distributedValue.Entry;
                    }
                }
            }
            catch (Exception exception) when (exception is not OperationCanceledException)
            {
                // Tenant resolution must fall back to PostgreSQL when Redis is unavailable.
            }
        }

        await using var command = dataSource.CreateCommand(sql);
        command.Parameters.AddWithValue("lookup", lookup);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        if (!await reader.ReadAsync(cancellationToken))
        {
            await StoreAsync(distributedCache, cacheKey, new CacheEnvelope(false, null), TimeSpan.FromSeconds(20), cancellationToken);
            return null;
        }

        var result = new TenantDirectoryEntry(
            reader.GetGuid(0),
            reader.GetString(1),
            reader.GetString(2),
            ParseEnum<TenantStatus>(reader.GetString(3)),
            ParseEnum<TenantMode>(reader.GetString(4)),
            reader.IsDBNull(5) ? null : reader.GetString(5));
        await StoreAsync(distributedCache, cacheKey, new CacheEnvelope(true, result), TimeSpan.FromSeconds(300), cancellationToken);
        return result;
    }

    private async Task StoreAsync(
        IDistributedCache? distributedCache,
        string cacheKey,
        CacheEnvelope value,
        TimeSpan distributedDuration,
        CancellationToken cancellationToken)
    {
        memoryCache.Set(cacheKey, value, value.Found ? TimeSpan.FromSeconds(30) : TimeSpan.FromSeconds(20));
        if (distributedCache is null)
        {
            return;
        }

        try
        {
            await distributedCache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(value, SerializerOptions),
                new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = distributedDuration },
                cancellationToken);
        }
        catch (Exception exception) when (exception is not OperationCanceledException)
        {
            // L1 remains usable and the next miss will fall back to PostgreSQL.
        }
    }

    private static TEnum ParseEnum<TEnum>(string value)
        where TEnum : struct, Enum
    {
        return Enum.Parse<TEnum>(value.Replace("_", string.Empty, StringComparison.Ordinal), true);
    }

    private static string Normalize(string value) => value.Trim().ToLowerInvariant();
}
