using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Tiku.Application.Tenancy;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Tenancy;

public sealed class DnsDomainOwnershipVerifier(
    HttpClient httpClient,
    IOptions<DomainLifecycleOptions> options) : IDomainOwnershipVerifier
{
    private readonly DomainLifecycleOptions options = options.Value;

    public async Task<DomainOwnershipResult> VerifyAsync(
        string host,
        string verificationToken,
        CancellationToken cancellationToken = default)
    {
        if (options.AllowedCnameTargets.Length == 0 || string.IsNullOrWhiteSpace(options.DnsJsonEndpoint))
        {
            return new(false, false, "DNS verification is not configured.");
        }

        try
        {
            var cnameAnswers = await QueryAsync(host, "CNAME", cancellationToken);
            var cnameMatches = cnameAnswers.Any(answer => options.AllowedCnameTargets.Any(target =>
                NormalizeDnsName(answer).Equals(NormalizeDnsName(target), StringComparison.OrdinalIgnoreCase)));
            if (!cnameMatches)
            {
                return new(false, true, "CNAME does not point to an allowed gateway target.");
            }

            var verificationName = $"{options.VerificationRecordPrefix.Trim().TrimEnd('.')}.{host}";
            var txtAnswers = await QueryAsync(verificationName, "TXT", cancellationToken);
            var txtMatches = txtAnswers.Any(answer =>
                answer.Trim().Trim('"').Equals(verificationToken, StringComparison.Ordinal));
            return txtMatches
                ? new(true, true, null)
                : new(false, true, "TXT ownership token was not found.");
        }
        catch (Exception exception) when (exception is HttpRequestException or JsonException or TaskCanceledException)
        {
            return new(false, true, $"DNS verification failed: {exception.Message}");
        }
    }

    private async Task<string[]> QueryAsync(string name, string type, CancellationToken cancellationToken)
    {
        var endpoint = options.DnsJsonEndpoint.TrimEnd('/');
        using var request = new HttpRequestMessage(
            HttpMethod.Get,
            $"{endpoint}?name={Uri.EscapeDataString(name)}&type={type}");
        request.Headers.Accept.ParseAdd("application/dns-json");
        using var response = await httpClient.SendAsync(request, cancellationToken);
        response.EnsureSuccessStatusCode();
        using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
        if (!document.RootElement.TryGetProperty("Answer", out var answers) || answers.ValueKind != JsonValueKind.Array)
        {
            return [];
        }

        return answers.EnumerateArray()
            .Where(answer => answer.TryGetProperty("data", out _))
            .Select(answer => answer.GetProperty("data").GetString() ?? string.Empty)
            .Where(answer => answer.Length > 0)
            .ToArray();
    }

    private static string NormalizeDnsName(string value) => value.Trim().Trim('"').TrimEnd('.');
}

public sealed class HttpDomainGatewayProvisioner(
    HttpClient httpClient,
    IOptions<DomainLifecycleOptions> options) : IDomainGatewayProvisioner
{
    private readonly DomainLifecycleOptions options = options.Value;

    public async Task<DomainGatewayResult> EnsureTlsAsync(
        string host,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(options.GatewayBaseUrl) || string.IsNullOrWhiteSpace(options.GatewayApiKey))
        {
            return new(false, false, "Gateway TLS provisioning is not configured.");
        }

        try
        {
            using var request = new HttpRequestMessage(
                HttpMethod.Post,
                $"{options.GatewayBaseUrl.TrimEnd('/')}/domains/ensure");
            request.Headers.Authorization = new("Bearer", options.GatewayApiKey);
            request.Content = JsonContent.Create(new { host });
            using var response = await httpClient.SendAsync(request, cancellationToken);
            if (!response.IsSuccessStatusCode)
            {
                return new(false, true, $"Gateway returned HTTP {(int)response.StatusCode}.");
            }

            using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
            var tlsReady = document.RootElement.TryGetProperty("tlsReady", out var value) && value.GetBoolean();
            return tlsReady
                ? new(true, true, null)
                : new(false, true, "Gateway route exists but TLS is not ready.");
        }
        catch (Exception exception) when (exception is HttpRequestException or JsonException or TaskCanceledException)
        {
            return new(false, true, $"Gateway provisioning failed: {exception.Message}");
        }
    }
}

public sealed class TenantRuntimeCacheInvalidator(
    IMemoryCache cache,
    ITenantPublicCacheInvalidator publicCacheInvalidator) : ITenantRuntimeCacheInvalidator
{
    public async Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken = default)
    {
        cache.Remove($"tenant-runtime:{tenantId:N}");
        await publicCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
    }
}

public sealed class TenantDomainLifecycleService(
    TikuDbContext dbContext,
    IDomainOwnershipVerifier ownershipVerifier,
    IDomainGatewayProvisioner gatewayProvisioner,
    ITenantRuntimeCacheInvalidator cacheInvalidator,
    IOptions<DomainLifecycleOptions> options) : ITenantDomainLifecycleService
{
    private readonly DomainLifecycleOptions options = options.Value;

    public async Task<int> ProcessPendingAsync(CancellationToken cancellationToken = default)
    {
        if (!options.Enabled)
        {
            return 0;
        }

        var domains = await dbContext.TenantDomains
            .Where(domain =>
                domain.DomainType == TenantDomainType.Custom &&
                (domain.Status == TenantDomainStatus.Pending || domain.Status == TenantDomainStatus.Failed))
            .OrderBy(domain => domain.LastCheckedAt)
            .Take(Math.Clamp(options.BatchSize, 1, 500))
            .ToArrayAsync(cancellationToken);

        foreach (var domain in domains)
        {
            await ProcessAsync(domain, cancellationToken);
        }

        if (domains.Length > 0)
        {
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return domains.Length;
    }

    private async Task ProcessAsync(TenantDomain domain, CancellationToken cancellationToken)
    {
        domain.LastCheckedAt = DateTimeOffset.UtcNow;
        if (string.IsNullOrWhiteSpace(domain.VerificationToken))
        {
            Fail(domain, true, "Domain verification token is missing.");
            return;
        }

        var ownership = await ownershipVerifier.VerifyAsync(domain.Host, domain.VerificationToken, cancellationToken);
        if (!ownership.Verified)
        {
            Fail(domain, ownership.Configured, ownership.FailureReason);
            return;
        }

        domain.DnsVerifiedAt ??= DateTimeOffset.UtcNow;
        domain.VerifiedAt ??= domain.DnsVerifiedAt;
        var gateway = await gatewayProvisioner.EnsureTlsAsync(domain.Host, cancellationToken);
        if (!gateway.TlsReady)
        {
            Fail(domain, gateway.Configured, gateway.FailureReason);
            return;
        }

        domain.TlsReadyAt ??= DateTimeOffset.UtcNow;
        domain.Status = TenantDomainStatus.Active;
        domain.LastFailureReason = null;
        await cacheInvalidator.InvalidateAsync(domain.TenantId, cancellationToken);
    }

    private static void Fail(TenantDomain domain, bool configured, string? reason)
    {
        domain.Status = configured ? TenantDomainStatus.Failed : TenantDomainStatus.Pending;
        domain.LastFailureReason = reason;
    }
}
